From 173b662d6565e14c6e1e45fa12ea1cdea0d632f5 Mon Sep 17 00:00:00 2001 From: ivanhromovyi Date: Wed, 7 May 2025 23:29:04 +0200 Subject: [PATCH] added solscanService, defiMonitorBot and exceptions --- pom.xml | 58 +++++- .../solscanbot/bot/DeFiMonitorBotImpl.java | 80 -------- .../ivan/solscanbot/config/MapperConfig.java | 12 ++ .../solscanbot/config/SchedulingConfig.java | 9 + .../solscanbot/config/TelegramBotConfig.java | 5 +- .../external/SingleTokenNameResponseDto.java | 10 + .../dto/external/TokenNamesResponseDto.java | 9 + .../dto/internal/MonitoredAddress.java | 42 ++++ .../ivan/solscanbot/dto/internal/Token.java | 26 +++ .../AddressAlreadyExistsException.java | 11 ++ .../AddressNotMonitoredException.java | 12 ++ .../exception/InvalidAddressException.java | 11 ++ ...NotHaveAnyMonitoredAddressesException.java | 11 ++ .../ivan/solscanbot/mapper/TokenMapper.java | 11 ++ .../solscanbot/model/MonitoredAddress.java | 14 -- .../MonitoredAddressRepository.java | 18 ++ .../repository/TokenRepository.java | 9 + .../solscanbot/service/DeFiMonitorBot.java | 185 ++++++++++++++++++ .../solscanbot/service/MonitoringService.java | 35 ++++ .../solscanbot/service/SolScanService.java | 8 + .../service/SolScanServiceImpl.java | 43 ++++ src/main/resources/application.properties | 9 + .../SolScanBotApplicationTests.java | 9 +- src/test/resources/application.properties | 12 +- 24 files changed, 547 insertions(+), 102 deletions(-) delete mode 100644 src/main/java/ivan/solscanbot/bot/DeFiMonitorBotImpl.java create mode 100644 src/main/java/ivan/solscanbot/config/MapperConfig.java create mode 100644 src/main/java/ivan/solscanbot/config/SchedulingConfig.java create mode 100644 src/main/java/ivan/solscanbot/dto/external/SingleTokenNameResponseDto.java create mode 100644 src/main/java/ivan/solscanbot/dto/external/TokenNamesResponseDto.java create mode 100644 src/main/java/ivan/solscanbot/dto/internal/MonitoredAddress.java create mode 100644 src/main/java/ivan/solscanbot/dto/internal/Token.java create mode 100644 src/main/java/ivan/solscanbot/exception/AddressAlreadyExistsException.java create mode 100644 src/main/java/ivan/solscanbot/exception/AddressNotMonitoredException.java create mode 100644 src/main/java/ivan/solscanbot/exception/InvalidAddressException.java create mode 100644 src/main/java/ivan/solscanbot/exception/UserNotHaveAnyMonitoredAddressesException.java create mode 100644 src/main/java/ivan/solscanbot/mapper/TokenMapper.java delete mode 100644 src/main/java/ivan/solscanbot/model/MonitoredAddress.java create mode 100644 src/main/java/ivan/solscanbot/repository/MonitoredAddressRepository.java create mode 100644 src/main/java/ivan/solscanbot/repository/TokenRepository.java create mode 100644 src/main/java/ivan/solscanbot/service/DeFiMonitorBot.java create mode 100644 src/main/java/ivan/solscanbot/service/MonitoringService.java create mode 100644 src/main/java/ivan/solscanbot/service/SolScanService.java create mode 100644 src/main/java/ivan/solscanbot/service/SolScanServiceImpl.java diff --git a/pom.xml b/pom.xml index 426e2ae..30b56d6 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent 3.4.5 - + ivan solscanbot @@ -29,6 +29,8 @@ checkstyle.xml 21 + 1.6.3 + 0.2.0 @@ -55,6 +57,11 @@ telegrambots 6.1.0 + + org.mapstruct + mapstruct + ${org.mapstruct.version} + org.springframework.boot @@ -74,7 +81,54 @@ org.springframework.boot spring-boot-maven-plugin + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.0 + + + compile + + check + + + + + + ${project.build.sourceDirectory} + ${project.build.testSourceDirectory} + + ${maven.checkstyle.plugin.configLocation} + true + true + false + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + ${lombok.mapstruct.binding.version} + + + org.mapstruct + mapstruct-processor + ${org.mapstruct.version} + + + + - diff --git a/src/main/java/ivan/solscanbot/bot/DeFiMonitorBotImpl.java b/src/main/java/ivan/solscanbot/bot/DeFiMonitorBotImpl.java deleted file mode 100644 index 022ce8d..0000000 --- a/src/main/java/ivan/solscanbot/bot/DeFiMonitorBotImpl.java +++ /dev/null @@ -1,80 +0,0 @@ -package ivan.solscanbot.bot; - -import lombok.RequiredArgsConstructor; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Component; -import org.telegram.telegrambots.bots.TelegramLongPollingBot; -import org.telegram.telegrambots.meta.api.methods.send.SendMessage; -import org.telegram.telegrambots.meta.api.objects.Update; -import org.telegram.telegrambots.meta.exceptions.TelegramApiException; - -@Component -public class DeFiMonitorBotImpl extends TelegramLongPollingBot { - private final String token; - private final String username; - - public DeFiMonitorBotImpl( - @Value("${telegram.bot.token}") String token, - @Value("${telegram.bot.username}") String username - ) { - this.token = token; - this.username = username; - } - - @Override - public String getBotUsername() { - return username; - } - - @Override - public String getBotToken() { - return token; - } - - @Override - public void onUpdateReceived(Update update) { - if (update.hasMessage() && update.getMessage().hasText()) { - String messageText = update.getMessage().getText(); - long chatId = update.getMessage().getChatId(); - - if (messageText.startsWith("/add")) { - handleAddAddress(chatId, messageText); - } else if (messageText.startsWith("/list")) { - handleListAddresses(chatId); - } else if (messageText.startsWith("/remove")) { - handleRemoveAddress(chatId, messageText); - } else { - sendMessage(chatId, "Unknown command. Use /add, /list, or /remove"); - } - } - } - - private void handleAddAddress(long chatId, String message) { - sendMessage(chatId, "Address added to monitoring"); - } - - private void handleListAddresses(long chatId) { - sendMessage(chatId, "List of monitored addresses..."); - } - - private void handleRemoveAddress(long chatId, String message) { - sendMessage(chatId, "Address removed from monitoring"); - } - - public void sendNotification(long chatId, String notification) { - sendMessage(chatId, notification); - } - - private void sendMessage(long chatId, String text) { - SendMessage message = new SendMessage(); - message.setChatId(String.valueOf(chatId)); - message.setText(text); - - try { - execute(message); - } catch (TelegramApiException e) { - e.printStackTrace(); - } - } - -} diff --git a/src/main/java/ivan/solscanbot/config/MapperConfig.java b/src/main/java/ivan/solscanbot/config/MapperConfig.java new file mode 100644 index 0000000..80e401e --- /dev/null +++ b/src/main/java/ivan/solscanbot/config/MapperConfig.java @@ -0,0 +1,12 @@ +package ivan.solscanbot.config; + +import org.mapstruct.InjectionStrategy; +import org.mapstruct.NullValueCheckStrategy; + +@org.mapstruct.MapperConfig( + componentModel = "spring", + injectionStrategy = InjectionStrategy.CONSTRUCTOR, + nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, + implementationPackage = ".impl" +)public class MapperConfig { +} diff --git a/src/main/java/ivan/solscanbot/config/SchedulingConfig.java b/src/main/java/ivan/solscanbot/config/SchedulingConfig.java new file mode 100644 index 0000000..ef1de28 --- /dev/null +++ b/src/main/java/ivan/solscanbot/config/SchedulingConfig.java @@ -0,0 +1,9 @@ +package ivan.solscanbot.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; + +@Configuration +@EnableScheduling +public class SchedulingConfig { +} diff --git a/src/main/java/ivan/solscanbot/config/TelegramBotConfig.java b/src/main/java/ivan/solscanbot/config/TelegramBotConfig.java index 8cd4a41..c626ea1 100644 --- a/src/main/java/ivan/solscanbot/config/TelegramBotConfig.java +++ b/src/main/java/ivan/solscanbot/config/TelegramBotConfig.java @@ -1,7 +1,6 @@ package ivan.solscanbot.config; -import ivan.solscanbot.bot.DeFiMonitorBotImpl; -import org.springframework.beans.factory.annotation.Value; +import ivan.solscanbot.service.DeFiMonitorBot; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.telegram.telegrambots.meta.TelegramBotsApi; @@ -11,7 +10,7 @@ @Configuration public class TelegramBotConfig { @Bean - public TelegramBotsApi telegramBotsApi(DeFiMonitorBotImpl bot) throws TelegramApiException { + public TelegramBotsApi telegramBotsApi(DeFiMonitorBot bot) throws TelegramApiException { TelegramBotsApi botsApi = new TelegramBotsApi(DefaultBotSession.class); botsApi.registerBot(bot); return botsApi; diff --git a/src/main/java/ivan/solscanbot/dto/external/SingleTokenNameResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/SingleTokenNameResponseDto.java new file mode 100644 index 0000000..be51bd1 --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/external/SingleTokenNameResponseDto.java @@ -0,0 +1,10 @@ +package ivan.solscanbot.dto.external; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Data; + +@Data +public class SingleTokenNameResponseDto { + @JsonProperty("token_name") + private String tokenName; +} diff --git a/src/main/java/ivan/solscanbot/dto/external/TokenNamesResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/TokenNamesResponseDto.java new file mode 100644 index 0000000..1f29840 --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/external/TokenNamesResponseDto.java @@ -0,0 +1,9 @@ +package ivan.solscanbot.dto.external; + +import java.util.Set; +import lombok.Data; + +@Data +public class TokenNamesResponseDto { + private Set tokens; +} diff --git a/src/main/java/ivan/solscanbot/dto/internal/MonitoredAddress.java b/src/main/java/ivan/solscanbot/dto/internal/MonitoredAddress.java new file mode 100644 index 0000000..e4acaed --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/internal/MonitoredAddress.java @@ -0,0 +1,42 @@ +package ivan.solscanbot.dto.internal; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import java.util.HashSet; +import java.util.Set; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Entity +@Data +@NoArgsConstructor +@SQLDelete(sql = "UPDATE roles SET is_deleted = true WHERE id = ?") +@Where(clause = "is_deleted = false") +public class MonitoredAddress { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private String address; + @Column(nullable = false) + private Long chatId; + @ManyToMany + @ToString.Exclude + @EqualsAndHashCode.Exclude + @JoinTable(name = "address_token", + joinColumns = @JoinColumn(name = "address_id"), + inverseJoinColumns = @JoinColumn(name = "token_id")) + private Set tokens = new HashSet<>(); + @Column(name = "is_deleted") + private boolean deleted = false; +} diff --git a/src/main/java/ivan/solscanbot/dto/internal/Token.java b/src/main/java/ivan/solscanbot/dto/internal/Token.java new file mode 100644 index 0000000..751720b --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/internal/Token.java @@ -0,0 +1,26 @@ +package ivan.solscanbot.dto.internal; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.Where; + +@Data +@Entity +@NoArgsConstructor +@SQLDelete(sql = "UPDATE roles SET is_deleted = true WHERE id = ?") +@Where(clause = "is_deleted = false") +public class Token { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(unique = true, nullable = false, name = "token_name") + private String tokenName; + @Column(name = "is_deleted") + private boolean isDeleted = false; +} diff --git a/src/main/java/ivan/solscanbot/exception/AddressAlreadyExistsException.java b/src/main/java/ivan/solscanbot/exception/AddressAlreadyExistsException.java new file mode 100644 index 0000000..f2f6960 --- /dev/null +++ b/src/main/java/ivan/solscanbot/exception/AddressAlreadyExistsException.java @@ -0,0 +1,11 @@ +package ivan.solscanbot.exception; + +public class AddressAlreadyExistsException extends RuntimeException { + public AddressAlreadyExistsException(String message) { + super(message); + } + + public AddressAlreadyExistsException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ivan/solscanbot/exception/AddressNotMonitoredException.java b/src/main/java/ivan/solscanbot/exception/AddressNotMonitoredException.java new file mode 100644 index 0000000..a6dc866 --- /dev/null +++ b/src/main/java/ivan/solscanbot/exception/AddressNotMonitoredException.java @@ -0,0 +1,12 @@ +package ivan.solscanbot.exception; + +public class AddressNotMonitoredException extends RuntimeException { + + public AddressNotMonitoredException(String message) { + super(message); + } + + public AddressNotMonitoredException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ivan/solscanbot/exception/InvalidAddressException.java b/src/main/java/ivan/solscanbot/exception/InvalidAddressException.java new file mode 100644 index 0000000..8833961 --- /dev/null +++ b/src/main/java/ivan/solscanbot/exception/InvalidAddressException.java @@ -0,0 +1,11 @@ +package ivan.solscanbot.exception; + +public class InvalidAddressException extends RuntimeException { + public InvalidAddressException(String message) { + super(message); + } + + public InvalidAddressException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ivan/solscanbot/exception/UserNotHaveAnyMonitoredAddressesException.java b/src/main/java/ivan/solscanbot/exception/UserNotHaveAnyMonitoredAddressesException.java new file mode 100644 index 0000000..0543a4a --- /dev/null +++ b/src/main/java/ivan/solscanbot/exception/UserNotHaveAnyMonitoredAddressesException.java @@ -0,0 +1,11 @@ +package ivan.solscanbot.exception; + +public class UserNotHaveAnyMonitoredAddressesException extends RuntimeException { + public UserNotHaveAnyMonitoredAddressesException(String message) { + super(message); + } + + public UserNotHaveAnyMonitoredAddressesException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/ivan/solscanbot/mapper/TokenMapper.java b/src/main/java/ivan/solscanbot/mapper/TokenMapper.java new file mode 100644 index 0000000..a63eaaa --- /dev/null +++ b/src/main/java/ivan/solscanbot/mapper/TokenMapper.java @@ -0,0 +1,11 @@ +package ivan.solscanbot.mapper; + +import ivan.solscanbot.config.MapperConfig; +import ivan.solscanbot.dto.external.SingleTokenNameResponseDto; +import ivan.solscanbot.dto.internal.Token; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface TokenMapper { + Token toModel(SingleTokenNameResponseDto dto); +} diff --git a/src/main/java/ivan/solscanbot/model/MonitoredAddress.java b/src/main/java/ivan/solscanbot/model/MonitoredAddress.java deleted file mode 100644 index 77e5293..0000000 --- a/src/main/java/ivan/solscanbot/model/MonitoredAddress.java +++ /dev/null @@ -1,14 +0,0 @@ -package ivan.solscanbot.model; - -import jakarta.persistence.Entity; -import jakarta.persistence.Id; -import lombok.Data; - -@Entity -@Data -public class MonitoredAddress { - @Id - public Long id; - public String address; - public String chatId; -} diff --git a/src/main/java/ivan/solscanbot/repository/MonitoredAddressRepository.java b/src/main/java/ivan/solscanbot/repository/MonitoredAddressRepository.java new file mode 100644 index 0000000..fe3d6e6 --- /dev/null +++ b/src/main/java/ivan/solscanbot/repository/MonitoredAddressRepository.java @@ -0,0 +1,18 @@ +package ivan.solscanbot.repository; + +import ivan.solscanbot.dto.internal.MonitoredAddress; +import java.util.List; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface MonitoredAddressRepository + extends JpaRepository { + + void deleteByAddressAndChatId(String address, Long chatId); + + boolean existsByAddressAndChatId(String address, Long chatId); + + List findByChatId(Long chatId); + + Optional findByAddress(String address); +} diff --git a/src/main/java/ivan/solscanbot/repository/TokenRepository.java b/src/main/java/ivan/solscanbot/repository/TokenRepository.java new file mode 100644 index 0000000..b16fd25 --- /dev/null +++ b/src/main/java/ivan/solscanbot/repository/TokenRepository.java @@ -0,0 +1,9 @@ +package ivan.solscanbot.repository; + +import ivan.solscanbot.dto.internal.Token; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface TokenRepository extends JpaRepository { + Optional findByTokenName(String tokenName); +} diff --git a/src/main/java/ivan/solscanbot/service/DeFiMonitorBot.java b/src/main/java/ivan/solscanbot/service/DeFiMonitorBot.java new file mode 100644 index 0000000..7efb014 --- /dev/null +++ b/src/main/java/ivan/solscanbot/service/DeFiMonitorBot.java @@ -0,0 +1,185 @@ +package ivan.solscanbot.service; + +import ivan.solscanbot.dto.internal.MonitoredAddress; +import ivan.solscanbot.dto.internal.Token; +import ivan.solscanbot.exception.AddressAlreadyExistsException; +import ivan.solscanbot.exception.AddressNotMonitoredException; +import ivan.solscanbot.exception.InvalidAddressException; +import ivan.solscanbot.exception.UserNotHaveAnyMonitoredAddressesException; +import ivan.solscanbot.mapper.TokenMapper; +import ivan.solscanbot.repository.MonitoredAddressRepository; +import ivan.solscanbot.repository.TokenRepository; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.telegram.telegrambots.bots.TelegramLongPollingBot; +import org.telegram.telegrambots.meta.api.methods.send.SendMessage; +import org.telegram.telegrambots.meta.api.objects.Update; +import org.telegram.telegrambots.meta.exceptions.TelegramApiException; + +@Component +public class DeFiMonitorBot extends TelegramLongPollingBot { + private static final String SOLANA_ADDRESS_PATTERN = "[1-9A-HJ-NP-Za-km-z]{32,44}"; + + private final String token; + private final String username; + private final MonitoredAddressRepository addressRepository; + private final TokenRepository tokenRepository; + private final SolScanServiceImpl solScanService; + private final TokenMapper tokenMapper; + + public DeFiMonitorBot( + @Value("${telegram.bot.token}") String token, + @Value("${telegram.bot.username}") String username, + MonitoredAddressRepository addressRepository, TokenRepository tokenRepository, + SolScanServiceImpl solScanService, TokenMapper tokenMapper + ) { + this.token = token; + this.username = username; + this.addressRepository = addressRepository; + this.tokenRepository = tokenRepository; + this.solScanService = solScanService; + this.tokenMapper = tokenMapper; + } + + @Override + public String getBotUsername() { + return username; + } + + @Override + public String getBotToken() { + return token; + } + + @Override + public void onUpdateReceived(Update update) { + if (update.hasMessage() && update.getMessage().hasText()) { + String messageText = update.getMessage().getText(); + long chatId = update.getMessage().getChatId(); + + if (messageText.startsWith("/add")) { + handleAddAddress(chatId, messageText); + } else if (messageText.startsWith("/list")) { + handleListAddresses(chatId); + } else if (messageText.startsWith("/remove")) { + handleRemoveAddress(chatId, messageText); + } else if (messageText.startsWith("/portfolio")) { + handleAddressPortfolio(chatId, messageText); + } else { + sendMessage( + chatId, "Unknown command. Use /add, /list, or /remove"); + } + } + } + + public void sendMessage(long chatId, String text) { + SendMessage message = new SendMessage(); + message.setChatId(String.valueOf(chatId)); + message.setText(text); + try { + execute(message); + } catch (TelegramApiException e) { + throw new RuntimeException("Error while sending a message.", e); + } + } + + private void handleAddressPortfolio(long chatId, String messageText) { + String address = getSolanaAddressFromMessage(messageText); + verifySolanaAddress(chatId, address); + verifyUserHasCertainAddress(chatId, address); + + MonitoredAddress solanaAddress = addressRepository.findByAddress(address) + .orElseThrow(() -> new AddressNotMonitoredException(address)); + + String tokens = Optional.ofNullable(solanaAddress.getTokens()) + .orElse(Collections.emptySet()) + .stream() + .map(Token::getTokenName) + .collect(Collectors.joining(", ")); + + sendMessage(chatId, "Tokens in portfolio:\n" + tokens); + } + + public void handleAddAddress(long chatId, String message) { + String solanaAddress = getSolanaAddressFromMessage(message); + verifySolanaAddress(chatId, solanaAddress); + verifyAddressAlreadyAdded(chatId, solanaAddress); + Set tokensFromSolScan = solScanService.getTokensByAddress(solanaAddress) + .stream() + .map(tokenMapper::toModel) + .collect(Collectors.toSet()); + Set managedTokens = tokensFromSolScan.stream() + .map(t -> tokenRepository.findByTokenName(t.getTokenName()) + .orElseGet(() -> tokenRepository.save(t))) + .collect(Collectors.toSet()); + MonitoredAddress address = new MonitoredAddress(); + address.setAddress(solanaAddress); + address.setChatId(chatId); + address.setTokens(managedTokens); + addressRepository.save(address); + sendMessage(chatId, "address\n" + solanaAddress + "\nis added to monitoring"); + } + + private void handleListAddresses(long chatId) { + List addresses = addressRepository.findByChatId(chatId); + verifyUserHasAddresses(chatId, addresses); + AtomicInteger count = new AtomicInteger(1); + String addressList = addresses + .stream() + .map(adr -> count.getAndIncrement() + ". " + adr.getAddress()) + .collect(Collectors.joining("\n")); + sendMessage(chatId, "List of monitored addresses:\n" + addressList); + } + + private void handleRemoveAddress(long chatId, String message) { + String solanaAddress = getSolanaAddressFromMessage(message); + verifySolanaAddress(chatId, solanaAddress); + addressRepository.deleteByAddressAndChatId(solanaAddress, chatId); + sendMessage( + chatId, "address\n" + solanaAddress + "\nis removed from monitoring"); + } + + public void sendNotification(long chatId, String notification) { + sendMessage(chatId, notification); + } + + private String getSolanaAddressFromMessage(String message) { + return message.substring(4).trim(); + } + + private void verifySolanaAddress(long chatId, String address) { + if (address == null || !address.matches(SOLANA_ADDRESS_PATTERN)) { + sendMessage(chatId, "Invalid Solana Address"); + throw new InvalidAddressException("Invalid Solana Address"); + } + } + + private void verifyAddressAlreadyAdded(long chatId, String address) { + if (addressRepository.existsByAddressAndChatId(address, chatId)) { + sendMessage(chatId,"You already have this address in your list"); + throw new AddressAlreadyExistsException("You already have this address in your list"); + } + } + + private void verifyUserHasAddresses(long chatId, List addresses) { + if (addresses.isEmpty()) { + sendMessage(chatId, "You have no monitored addresses"); + throw new UserNotHaveAnyMonitoredAddressesException("You have no monitored addresses"); + } + } + + private void verifyUserHasCertainAddress(long chatId, String address) { + if (!addressRepository.existsByAddressAndChatId(address, chatId)) { + sendMessage(chatId, + "You need to add this address to monitored address first '/add your_address'"); + throw new AddressNotMonitoredException( + "You need to add this address to monitored address first '/add your_address'"); + } + } +} diff --git a/src/main/java/ivan/solscanbot/service/MonitoringService.java b/src/main/java/ivan/solscanbot/service/MonitoringService.java new file mode 100644 index 0000000..d09f1f5 --- /dev/null +++ b/src/main/java/ivan/solscanbot/service/MonitoringService.java @@ -0,0 +1,35 @@ +package ivan.solscanbot.service; + +import ivan.solscanbot.dto.internal.MonitoredAddress; +import ivan.solscanbot.repository.MonitoredAddressRepository; +import org.jvnet.hk2.annotations.Service; +import org.springframework.scheduling.annotation.Scheduled; + +@Service +public class MonitoringService { + private final MonitoredAddressRepository addressRepository; + private final DeFiMonitorBot deFiMonitorBot; + private final SolScanServiceImpl solScanService; + + public MonitoringService( + MonitoredAddressRepository addressRepository, + DeFiMonitorBot deFiMonitorBot, + SolScanServiceImpl solScanService) { + this.addressRepository = addressRepository; + this.deFiMonitorBot = deFiMonitorBot; + this.solScanService = solScanService; + } + + @Scheduled(fixedRate = 15000) + public void sendTokenList() { + for (MonitoredAddress address : addressRepository.findAll()) { + try { + String notification = "Tokens for address: " + address.getAddress() + + solScanService.getTokensByAddress(address.getAddress()); + deFiMonitorBot.sendNotification(address.getId(), notification); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/src/main/java/ivan/solscanbot/service/SolScanService.java b/src/main/java/ivan/solscanbot/service/SolScanService.java new file mode 100644 index 0000000..84c455b --- /dev/null +++ b/src/main/java/ivan/solscanbot/service/SolScanService.java @@ -0,0 +1,8 @@ +package ivan.solscanbot.service; + +import ivan.solscanbot.dto.external.SingleTokenNameResponseDto; +import java.util.Set; + +public interface SolScanService { + Set getTokensByAddress(String address); +} diff --git a/src/main/java/ivan/solscanbot/service/SolScanServiceImpl.java b/src/main/java/ivan/solscanbot/service/SolScanServiceImpl.java new file mode 100644 index 0000000..c74a10b --- /dev/null +++ b/src/main/java/ivan/solscanbot/service/SolScanServiceImpl.java @@ -0,0 +1,43 @@ +package ivan.solscanbot.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import ivan.solscanbot.dto.external.SingleTokenNameResponseDto; +import ivan.solscanbot.dto.external.TokenNamesResponseDto; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class SolScanServiceImpl implements SolScanService { + private static final String SOL_SCAN_URL = "https://pro-api.solscan.io/v2.0/account/portfolio"; + @Value("${sol.scan.key}") + private String solScanKey; + private final ObjectMapper objectMapper; + + @Override + public Set getTokensByAddress(String address) { + HttpClient httpClient = HttpClient.newHttpClient(); + String url = SOL_SCAN_URL + "?address=" + address; + HttpRequest httpRequest = HttpRequest.newBuilder() + .GET() + .uri(URI.create(url)) + .header("accept", "application/json") + .header("token", solScanKey) + .build(); + try { + HttpResponse response = + httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); + TokenNamesResponseDto tokens = + objectMapper.readValue(response.body(), TokenNamesResponseDto.class); + return tokens.getTokens(); + } catch (Exception e) { + throw new RuntimeException("An error occurred. Please try again later."); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 68bbb65..7709139 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,14 @@ spring.application.name=solscanbot +spring.thymeleaf.check-template-location=false +spring.jpa.open-in-view=false + +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console +spring.datasource.url=jdbc:h2:mem:scanbot + telegram.bot.token=${TELEGRAM_BOT_TOKEN} telegram.bot.username=${TELEGRAM_BOT_USERNAME} +sol.scan.url=${SOL_SCAN_URL} +sol.scan.key=${SOL_SCAN_KEY} spring.config.import=optional:file:.env[.properties] diff --git a/src/test/java/ivan/solscanbot/SolScanBotApplicationTests.java b/src/test/java/ivan/solscanbot/SolScanBotApplicationTests.java index 74b1736..0f6c821 100644 --- a/src/test/java/ivan/solscanbot/SolScanBotApplicationTests.java +++ b/src/test/java/ivan/solscanbot/SolScanBotApplicationTests.java @@ -1,8 +1,15 @@ package ivan.solscanbot; +import ivan.solscanbot.service.DeFiMonitorBot; +import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.telegram.telegrambots.meta.TelegramBotsApi; @SpringBootTest +@MockitoBean(types = {DeFiMonitorBot.class, TelegramBotsApi.class}) class SolScanBotApplicationTests { - + @Test + void contextLoads() { + } } diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index b9473c4..adeb4a3 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,2 +1,10 @@ -telegram.bot.token=token -telegram.bot.username=username +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect + +telegram.bot.token=bot_test +telegram.bot.username=user_test +sol.scan.url=url_test +sol.scan.key=key_test