diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bf98880c0..ad24d7a0aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features +- [#8966](https://github.com/blockscout/blockscout/pull/8966) - Add `ACCOUNT_WATCHLIST_NOTIFICATIONS_LIMIT_FOR_30_DAYS` + ### Fixes ### Chore diff --git a/apps/explorer/lib/explorer/account/notifier/notify.ex b/apps/explorer/lib/explorer/account/notifier/notify.ex index 70e633be8d..f9c9232546 100644 --- a/apps/explorer/lib/explorer/account/notifier/notify.ex +++ b/apps/explorer/lib/explorer/account/notifier/notify.ex @@ -55,7 +55,8 @@ defmodule Explorer.Account.Notifier.Notify do defp notify_watchlists(nil), do: nil defp notify_watchlist(%WatchlistAddress{} = address, summary, direction) do - case ForbiddenAddress.check(address.address_hash) do + case !WatchlistNotification.limit_reached_for_watchlist_id?(address.watchlist_id) && + ForbiddenAddress.check(address.address_hash) do {:ok, _address_hash} -> with %WatchlistNotification{} = notification <- build_watchlist_notification( @@ -74,6 +75,9 @@ defmodule Explorer.Account.Notifier.Notify do {:error, _message} -> nil + + false -> + nil end end @@ -106,9 +110,6 @@ defmodule Explorer.Account.Notifier.Notify do Logger.info("--- email delivery response: FAILED", fetcher: :account) Logger.info(error, fetcher: :account) end - else - Logger.info("--- email delivery response: FAILED", fetcher: :account) - Logger.info("Email is not composed (is nil)", fetcher: :account) end end @@ -119,6 +120,7 @@ defmodule Explorer.Account.Notifier.Notify do if is_watched(address, summary, direction) do %WatchlistNotification{ watchlist_address_id: address.id, + watchlist_id: address.watchlist_id, transaction_hash: summary.transaction_hash, from_address_hash: summary.from_address_hash, to_address_hash: summary.to_address_hash, diff --git a/apps/explorer/lib/explorer/account/watchlist_notification.ex b/apps/explorer/lib/explorer/account/watchlist_notification.ex index 935e532187..cc45561073 100644 --- a/apps/explorer/lib/explorer/account/watchlist_notification.ex +++ b/apps/explorer/lib/explorer/account/watchlist_notification.ex @@ -1,6 +1,6 @@ defmodule Explorer.Account.WatchlistNotification do @moduledoc """ - Stored notification about event + Stored notification about event related to WatchlistAddress """ @@ -9,7 +9,8 @@ defmodule Explorer.Account.WatchlistNotification do import Ecto.Changeset import Explorer.Chain, only: [hash_to_lower_case_string: 1] - alias Explorer.Account.WatchlistAddress + alias Explorer.Repo + alias Explorer.Account.{Watchlist, WatchlistAddress} schema "account_watchlist_notifications" do field(:amount, :decimal) @@ -24,6 +25,7 @@ defmodule Explorer.Account.WatchlistNotification do field(:subject_hash, Cloak.Ecto.SHA256) belongs_to(:watchlist_address, WatchlistAddress) + belongs_to(:watchlist, Watchlist) field(:from_address_hash, Explorer.Encrypted.AddressHash) field(:to_address_hash, Explorer.Encrypted.AddressHash) @@ -62,4 +64,23 @@ defmodule Explorer.Account.WatchlistNotification do |> put_change(:transaction_hash_hash, hash_to_lower_case_string(get_field(changeset, :transaction_hash))) |> put_change(:subject_hash, get_field(changeset, :subject)) end + + @doc """ + Check if amount of watchlist notifications for the last 30 days is less than ACCOUNT_WATCHLIST_NOTIFICATIONS_LIMIT_FOR_30_DAYS + """ + @spec limit_reached_for_watchlist_id?(integer) :: boolean + def limit_reached_for_watchlist_id?(watchlist_id) do + __MODULE__ + |> where( + [wn], + wn.watchlist_id == ^watchlist_id and + fragment("NOW() - ? at time zone 'UTC' <= interval '30 days'", wn.inserted_at) + ) + |> limit(^watchlist_notification_30_days_limit()) + |> Repo.account_repo().aggregate(:count) == watchlist_notification_30_days_limit() + end + + defp watchlist_notification_30_days_limit do + Application.get_env(:explorer, Explorer.Account)[:notifications_limit_for_30_days] + end end diff --git a/apps/explorer/priv/account/migrations/20231207201701_add_watchlist_id_column.exs b/apps/explorer/priv/account/migrations/20231207201701_add_watchlist_id_column.exs new file mode 100644 index 0000000000..346c9ec05e --- /dev/null +++ b/apps/explorer/priv/account/migrations/20231207201701_add_watchlist_id_column.exs @@ -0,0 +1,23 @@ +defmodule Explorer.Repo.Account.Migrations.AddWatchlistIdColumn do + use Ecto.Migration + + def change do + execute(""" + ALTER TABLE public.account_watchlist_notifications + DROP CONSTRAINT account_watchlist_notifications_watchlist_address_id_fkey; + """) + + alter table(:account_watchlist_notifications) do + add(:watchlist_id, :bigserial) + end + + create(index(:account_watchlist_notifications, [:watchlist_id])) + + execute(""" + UPDATE account_watchlist_notifications awn + SET watchlist_id = awa.watchlist_id + FROM account_watchlist_addresses awa + WHERE awa.id = awn.watchlist_address_id + """) + end +end diff --git a/apps/explorer/test/explorer/account/notifier/notify_test.exs b/apps/explorer/test/explorer/account/notifier/notify_test.exs index bc7480cc3e..860b569f2a 100644 --- a/apps/explorer/test/explorer/account/notifier/notify_test.exs +++ b/apps/explorer/test/explorer/account/notifier/notify_test.exs @@ -86,5 +86,61 @@ defmodule Explorer.Account.Notifier.NotifyTest do assert wn.tx_fee == fee assert wn.type == "COIN" end + + test "ignore new notification when limit is reached" do + old_envs = Application.get_env(:explorer, Explorer.Account) + + Application.put_env(:explorer, Explorer.Account, Keyword.put(old_envs, :notifications_limit_for_30_days, 1)) + + wa = + %WatchlistAddress{address_hash: address_hash} = + build(:account_watchlist_address, watch_coin_input: true) + |> Repo.account_repo().insert!() + + _watchlist_address = Repo.preload(wa, watchlist: :identity) + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction, to_address: %Chain.Address{hash: address_hash})) + + {_, fee} = Chain.fee(tx, :gwei) + amount = Wei.to(tx.value, :ether) + notify = Notify.call([tx]) + + wn = + WatchlistNotification + |> first + |> Repo.account_repo().one() + + assert notify == [[:ok]] + + assert wn.amount == amount + assert wn.direction == "incoming" + assert wn.method == "transfer" + assert wn.subject == "Coin transaction" + assert wn.tx_fee == fee + assert wn.type == "COIN" + address = Repo.get(Chain.Address, address_hash) + + tx = + %Transaction{ + from_address: _from_address, + to_address: _to_address, + block_number: _block_number, + hash: _tx_hash + } = with_block(insert(:transaction, to_address: address)) + + Notify.call([tx]) + + WatchlistNotification + |> first + |> Repo.account_repo().one!() + + Application.put_env(:explorer, Explorer.Account, old_envs) + end end end diff --git a/config/runtime.exs b/config/runtime.exs index 0f1ed1aa86..ec0cd0427b 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -407,7 +407,9 @@ config :explorer, Explorer.Account, ], resend_interval: ConfigHelper.parse_time_env_var("ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL", "5m"), private_tags_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_PRIVATE_TAGS_LIMIT", 2000), - watchlist_addresses_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_ADDRESSES_LIMIT", 15) + watchlist_addresses_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_ADDRESSES_LIMIT", 15), + notifications_limit_for_30_days: + ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_NOTIFICATIONS_LIMIT_FOR_30_DAYS", 1000) config :explorer, :token_id_migration, first_block: ConfigHelper.parse_integer_env_var("TOKEN_ID_MIGRATION_FIRST_BLOCK", 0), diff --git a/cspell.json b/cspell.json index 72bfc7609c..3cdf520ecd 100644 --- a/cspell.json +++ b/cspell.json @@ -545,7 +545,8 @@ "qitmeer", "meer", "DefiLlama", - "SOLIDITYSCAN" + "SOLIDITYSCAN", + "fkey" ], "enableFiletypes": [ "dotenv",