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