From ffe8485a39a35bcb09032a944dd1b64a010b30b5 Mon Sep 17 00:00:00 2001 From: ivanhromovyi Date: Sat, 10 May 2025 13:09:45 +0200 Subject: [PATCH] added BalanceActivity model and corresponding methods --- .../BalanceActivitiesResponseDto.java | 11 ++++ .../SingleBalanceActivityResponseDto.java | 19 ++++++ .../external/SingleTokenNameResponseDto.java | 10 --- .../SingleTokenPortfolioResponseDto.java | 17 +++++ .../dto/external/TokenMetaResponseDto.java | 9 +++ .../dto/external/TokenNamesResponseDto.java | 9 --- .../external/TokenPortfoliosResponseDto.java | 9 +++ .../dto/internal/BalanceActivity.java | 40 ++++++++++++ .../dto/internal/MonitoredAddress.java | 4 +- .../ivan/solscanbot/dto/internal/Token.java | 11 +++- .../solscanbot/mapper/ActivityMapper.java | 11 ++++ .../ivan/solscanbot/mapper/TokenMapper.java | 4 +- .../repository/ActivityRepository.java | 7 ++ .../solscanbot/service/MonitoringService.java | 51 +++++++++++---- .../solscanbot/service/SolScanService.java | 10 ++- .../service/SolScanServiceImpl.java | 64 +++++++++++++++++-- 16 files changed, 240 insertions(+), 46 deletions(-) create mode 100644 src/main/java/ivan/solscanbot/dto/external/BalanceActivitiesResponseDto.java create mode 100644 src/main/java/ivan/solscanbot/dto/external/SingleBalanceActivityResponseDto.java delete mode 100644 src/main/java/ivan/solscanbot/dto/external/SingleTokenNameResponseDto.java create mode 100644 src/main/java/ivan/solscanbot/dto/external/SingleTokenPortfolioResponseDto.java create mode 100644 src/main/java/ivan/solscanbot/dto/external/TokenMetaResponseDto.java delete mode 100644 src/main/java/ivan/solscanbot/dto/external/TokenNamesResponseDto.java create mode 100644 src/main/java/ivan/solscanbot/dto/external/TokenPortfoliosResponseDto.java create mode 100644 src/main/java/ivan/solscanbot/dto/internal/BalanceActivity.java create mode 100644 src/main/java/ivan/solscanbot/mapper/ActivityMapper.java create mode 100644 src/main/java/ivan/solscanbot/repository/ActivityRepository.java diff --git a/src/main/java/ivan/solscanbot/dto/external/BalanceActivitiesResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/BalanceActivitiesResponseDto.java new file mode 100644 index 0000000..b727b74 --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/external/BalanceActivitiesResponseDto.java @@ -0,0 +1,11 @@ +package ivan.solscanbot.dto.external; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Set; +import lombok.Data; + +@Data +public class BalanceActivitiesResponseDto { + @JsonProperty("data") + private Set activities; +} diff --git a/src/main/java/ivan/solscanbot/dto/external/SingleBalanceActivityResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/SingleBalanceActivityResponseDto.java new file mode 100644 index 0000000..121dab5 --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/external/SingleBalanceActivityResponseDto.java @@ -0,0 +1,19 @@ +package ivan.solscanbot.dto.external; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; +import java.util.Date; +import lombok.Data; + +@Data +public class SingleBalanceActivityResponseDto { + private String address; + @JsonProperty("token_address") + private String tokenAddress; + @JsonProperty("activity_type") + private String activityType; + private BigDecimal amount; + @JsonProperty("pre_balance") + private BigDecimal preBalance; + private Date time; +} diff --git a/src/main/java/ivan/solscanbot/dto/external/SingleTokenNameResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/SingleTokenNameResponseDto.java deleted file mode 100644 index be51bd1..0000000 --- a/src/main/java/ivan/solscanbot/dto/external/SingleTokenNameResponseDto.java +++ /dev/null @@ -1,10 +0,0 @@ -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/SingleTokenPortfolioResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/SingleTokenPortfolioResponseDto.java new file mode 100644 index 0000000..8c9f6d4 --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/external/SingleTokenPortfolioResponseDto.java @@ -0,0 +1,17 @@ +package ivan.solscanbot.dto.external; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.math.BigDecimal; +import lombok.Data; + +@Data +public class SingleTokenPortfolioResponseDto { + @JsonProperty("token_name") + private String tokenName; + @JsonProperty("token_address") + private String tokenAddress; + @JsonProperty("token_balance") + private String tokenBalance; + @JsonProperty("value") + private BigDecimal tokenValue; +} diff --git a/src/main/java/ivan/solscanbot/dto/external/TokenMetaResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/TokenMetaResponseDto.java new file mode 100644 index 0000000..3546ee4 --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/external/TokenMetaResponseDto.java @@ -0,0 +1,9 @@ +package ivan.solscanbot.dto.external; + +import lombok.Data; + +@Data +public class TokenMetaResponseDto { + private String name; + private String symbol; +} diff --git a/src/main/java/ivan/solscanbot/dto/external/TokenNamesResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/TokenNamesResponseDto.java deleted file mode 100644 index 1f29840..0000000 --- a/src/main/java/ivan/solscanbot/dto/external/TokenNamesResponseDto.java +++ /dev/null @@ -1,9 +0,0 @@ -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/external/TokenPortfoliosResponseDto.java b/src/main/java/ivan/solscanbot/dto/external/TokenPortfoliosResponseDto.java new file mode 100644 index 0000000..1517eda --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/external/TokenPortfoliosResponseDto.java @@ -0,0 +1,9 @@ +package ivan.solscanbot.dto.external; + +import java.util.Set; +import lombok.Data; + +@Data +public class TokenPortfoliosResponseDto { + private Set tokens; +} diff --git a/src/main/java/ivan/solscanbot/dto/internal/BalanceActivity.java b/src/main/java/ivan/solscanbot/dto/internal/BalanceActivity.java new file mode 100644 index 0000000..9e7bb1e --- /dev/null +++ b/src/main/java/ivan/solscanbot/dto/internal/BalanceActivity.java @@ -0,0 +1,40 @@ +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 java.math.BigDecimal; +import java.util.Date; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; +import org.hibernate.annotations.SQLDelete; +import org.hibernate.annotations.SQLRestriction; + +@Data +@Entity +@NoArgsConstructor +@Accessors(chain = true) +@SQLDelete(sql = "UPDATE roles SET is_deleted = true WHERE id = ?") +@SQLRestriction("is_deleted = false") +public class BalanceActivity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + @Column(nullable = false) + private String address; + @Column(nullable = false, name = "token_address") + private String tokenAddress; + @Column(nullable = false, name = "token_name") + private String tokenName; + @Column(nullable = false, name = "token_symbol") + private String tokenSymbol; + @Column(nullable = false) + private BigDecimal amount; + @Column(nullable = false) + private Date time; + @Column(nullable = false, name = "is_deleted") + private boolean isDeleted = false; +} diff --git a/src/main/java/ivan/solscanbot/dto/internal/MonitoredAddress.java b/src/main/java/ivan/solscanbot/dto/internal/MonitoredAddress.java index e4acaed..efb1df9 100644 --- a/src/main/java/ivan/solscanbot/dto/internal/MonitoredAddress.java +++ b/src/main/java/ivan/solscanbot/dto/internal/MonitoredAddress.java @@ -15,13 +15,13 @@ import lombok.NoArgsConstructor; import lombok.ToString; import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; +import org.hibernate.annotations.SQLRestriction; @Entity @Data @NoArgsConstructor @SQLDelete(sql = "UPDATE roles SET is_deleted = true WHERE id = ?") -@Where(clause = "is_deleted = false") +@SQLRestriction("is_deleted = false") public class MonitoredAddress { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/ivan/solscanbot/dto/internal/Token.java b/src/main/java/ivan/solscanbot/dto/internal/Token.java index 751720b..3611a6b 100644 --- a/src/main/java/ivan/solscanbot/dto/internal/Token.java +++ b/src/main/java/ivan/solscanbot/dto/internal/Token.java @@ -5,22 +5,29 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import java.math.BigDecimal; import lombok.Data; import lombok.NoArgsConstructor; import org.hibernate.annotations.SQLDelete; -import org.hibernate.annotations.Where; +import org.hibernate.annotations.SQLRestriction; @Data @Entity @NoArgsConstructor @SQLDelete(sql = "UPDATE roles SET is_deleted = true WHERE id = ?") -@Where(clause = "is_deleted = false") +@SQLRestriction("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(unique = true, nullable = false, name = "token_address") + private String tokenAddress; + @Column(unique = true, nullable = false, name = "token_balance") + private String tokenBalance; + @Column(unique = true, nullable = false, name = "token_value") + private BigDecimal tokenValue; @Column(name = "is_deleted") private boolean isDeleted = false; } diff --git a/src/main/java/ivan/solscanbot/mapper/ActivityMapper.java b/src/main/java/ivan/solscanbot/mapper/ActivityMapper.java new file mode 100644 index 0000000..87c1280 --- /dev/null +++ b/src/main/java/ivan/solscanbot/mapper/ActivityMapper.java @@ -0,0 +1,11 @@ +package ivan.solscanbot.mapper; + +import ivan.solscanbot.config.MapperConfig; +import ivan.solscanbot.dto.external.SingleBalanceActivityResponseDto; +import ivan.solscanbot.dto.internal.BalanceActivity; +import org.mapstruct.Mapper; + +@Mapper(config = MapperConfig.class) +public interface ActivityMapper { + BalanceActivity toModel(SingleBalanceActivityResponseDto activityDto); +} diff --git a/src/main/java/ivan/solscanbot/mapper/TokenMapper.java b/src/main/java/ivan/solscanbot/mapper/TokenMapper.java index a63eaaa..c199ff2 100644 --- a/src/main/java/ivan/solscanbot/mapper/TokenMapper.java +++ b/src/main/java/ivan/solscanbot/mapper/TokenMapper.java @@ -1,11 +1,11 @@ package ivan.solscanbot.mapper; import ivan.solscanbot.config.MapperConfig; -import ivan.solscanbot.dto.external.SingleTokenNameResponseDto; +import ivan.solscanbot.dto.external.SingleTokenPortfolioResponseDto; import ivan.solscanbot.dto.internal.Token; import org.mapstruct.Mapper; @Mapper(config = MapperConfig.class) public interface TokenMapper { - Token toModel(SingleTokenNameResponseDto dto); + Token toModel(SingleTokenPortfolioResponseDto dto); } diff --git a/src/main/java/ivan/solscanbot/repository/ActivityRepository.java b/src/main/java/ivan/solscanbot/repository/ActivityRepository.java new file mode 100644 index 0000000..5908f47 --- /dev/null +++ b/src/main/java/ivan/solscanbot/repository/ActivityRepository.java @@ -0,0 +1,7 @@ +package ivan.solscanbot.repository; + +import ivan.solscanbot.dto.internal.BalanceActivity; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ActivityRepository extends JpaRepository { +} diff --git a/src/main/java/ivan/solscanbot/service/MonitoringService.java b/src/main/java/ivan/solscanbot/service/MonitoringService.java index d09f1f5..5f39bd7 100644 --- a/src/main/java/ivan/solscanbot/service/MonitoringService.java +++ b/src/main/java/ivan/solscanbot/service/MonitoringService.java @@ -1,32 +1,59 @@ package ivan.solscanbot.service; +import ivan.solscanbot.dto.external.TokenMetaResponseDto; +import ivan.solscanbot.dto.internal.BalanceActivity; import ivan.solscanbot.dto.internal.MonitoredAddress; +import ivan.solscanbot.mapper.ActivityMapper; +import ivan.solscanbot.repository.ActivityRepository; import ivan.solscanbot.repository.MonitoredAddressRepository; +import java.math.BigDecimal; +import java.util.Set; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; import org.jvnet.hk2.annotations.Service; import org.springframework.scheduling.annotation.Scheduled; @Service +@RequiredArgsConstructor 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; - } + private final ActivityRepository activityRepository; + private final ActivityMapper activityMapper; @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); + Set activities = + solScanService.getNewBalanceActivities(address.getAddress()) + .stream() + .filter(a -> BigDecimal.ZERO.equals(a.getPreBalance())) + .map(activityMapper::toModel) + .collect(Collectors.toSet()); + if (!activities.isEmpty()) { + StringBuilder notification = new StringBuilder(); + notification.append("New activity for Solana address\n") + .append(address.getAddress()) + .append("was found"); + int count = 1; + for (BalanceActivity activity : activities) { + TokenMetaResponseDto meta = + solScanService.getTokenMetaFromAddress(address.getAddress()); + activity.setTokenName(meta.getName()) + .setTokenSymbol(meta.getSymbol()); + notification.append("\n").append(count++) + .append(". token address: ").append(activity.getTokenAddress()) + .append("\n") + .append(". token address: ").append(activity.getTokenAddress()) + .append("\n") + .append("token amount: ").append(activity.getAmount()).append("\n") + .append("time: ").append(activity.getTime()).append("\n"); + } + activityRepository.saveAll(activities); + deFiMonitorBot.sendNotification(address.getChatId(), notification.toString()); + } } 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 index 84c455b..28bb6a3 100644 --- a/src/main/java/ivan/solscanbot/service/SolScanService.java +++ b/src/main/java/ivan/solscanbot/service/SolScanService.java @@ -1,8 +1,14 @@ package ivan.solscanbot.service; -import ivan.solscanbot.dto.external.SingleTokenNameResponseDto; +import ivan.solscanbot.dto.external.SingleBalanceActivityResponseDto; +import ivan.solscanbot.dto.external.SingleTokenPortfolioResponseDto; +import ivan.solscanbot.dto.external.TokenMetaResponseDto; import java.util.Set; public interface SolScanService { - Set getTokensByAddress(String address); + Set getTokensByAddress(String address); + + Set getNewBalanceActivities(String address); + + TokenMetaResponseDto getTokenMetaFromAddress(String address); } diff --git a/src/main/java/ivan/solscanbot/service/SolScanServiceImpl.java b/src/main/java/ivan/solscanbot/service/SolScanServiceImpl.java index c74a10b..46aaa65 100644 --- a/src/main/java/ivan/solscanbot/service/SolScanServiceImpl.java +++ b/src/main/java/ivan/solscanbot/service/SolScanServiceImpl.java @@ -1,12 +1,16 @@ package ivan.solscanbot.service; import com.fasterxml.jackson.databind.ObjectMapper; -import ivan.solscanbot.dto.external.SingleTokenNameResponseDto; -import ivan.solscanbot.dto.external.TokenNamesResponseDto; +import ivan.solscanbot.dto.external.BalanceActivitiesResponseDto; +import ivan.solscanbot.dto.external.SingleBalanceActivityResponseDto; +import ivan.solscanbot.dto.external.SingleTokenPortfolioResponseDto; +import ivan.solscanbot.dto.external.TokenMetaResponseDto; +import ivan.solscanbot.dto.external.TokenPortfoliosResponseDto; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.time.Instant; import java.util.Set; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; @@ -15,15 +19,20 @@ @Service @RequiredArgsConstructor public class SolScanServiceImpl implements SolScanService { - private static final String SOL_SCAN_URL = "https://pro-api.solscan.io/v2.0/account/portfolio"; + private static final String SOL_SCAN_PORTFOLIO_URL = + "https://pro-api.solscan.io/v2.0/account/portfolio"; + private static final String SOL_SCAN_ACTIVITIES_URL = + "https://pro-api.solscan.io/v2.0/account/balance_change"; + private static final String SOL_SCAN_TOKEN_URL = + "https://pro-api.solscan.io/v2.0/token/meta"; @Value("${sol.scan.key}") private String solScanKey; private final ObjectMapper objectMapper; @Override - public Set getTokensByAddress(String address) { + public Set getTokensByAddress(String address) { HttpClient httpClient = HttpClient.newHttpClient(); - String url = SOL_SCAN_URL + "?address=" + address; + String url = SOL_SCAN_PORTFOLIO_URL + "?address=" + address; HttpRequest httpRequest = HttpRequest.newBuilder() .GET() .uri(URI.create(url)) @@ -33,11 +42,52 @@ public Set getTokensByAddress(String address) { try { HttpResponse response = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString()); - TokenNamesResponseDto tokens = - objectMapper.readValue(response.body(), TokenNamesResponseDto.class); + TokenPortfoliosResponseDto tokens = + objectMapper.readValue(response.body(), TokenPortfoliosResponseDto.class); return tokens.getTokens(); } catch (Exception e) { throw new RuntimeException("An error occurred. Please try again later."); } } + + @Override + public Set getNewBalanceActivities(String address) { + HttpClient httpClient = HttpClient.newHttpClient(); + long fromTime = Instant.now().getEpochSecond() - 60; + String url = SOL_SCAN_ACTIVITIES_URL + "?address=" + address + + "&from_time=" + fromTime; + 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()); + BalanceActivitiesResponseDto activities = + objectMapper.readValue(response.body(), BalanceActivitiesResponseDto.class); + return activities.getActivities(); + } catch (Exception e) { + throw new RuntimeException("An error occurred. Please try again later."); + } + } + + @Override + public TokenMetaResponseDto getTokenMetaFromAddress(String tokenAddress) { + String apiUrl = SOL_SCAN_TOKEN_URL + tokenAddress; + HttpClient client = HttpClient.newHttpClient(); + HttpRequest request = HttpRequest.newBuilder() + .uri(URI.create(apiUrl)) + .header("accept", "application/json") + .header("token", solScanKey) + .build(); + try { + HttpResponse response = client.send( + request, HttpResponse.BodyHandlers.ofString()); + return objectMapper.readValue(response.body(), TokenMetaResponseDto.class); + } catch (Exception e) { + throw new RuntimeException("Failed to get token name. Please try again later."); + } + } }