From b86b3da5d64cc5337ef2958c27a65f058d422cd9 Mon Sep 17 00:00:00 2001
From: mtyszczak <mateusz.tyszczak@gmail.com>
Date: Tue, 7 Jan 2025 18:03:02 +0100
Subject: [PATCH] Add on alarm filter and provider

---
 .../classifiers/account-classifier.ts         |  2 +
 .../change-recovery-in-progress-classifier.ts | 15 ++++
 .../decline-voting-rights-classifier.ts       | 14 ++++
 src/chain-observers/classifiers/index.ts      |  2 +
 .../collectors/jsonrpc/account-collector.ts   | 10 ++-
 .../change-recovery-in-progress-collector.ts  | 48 +++++++++++
 .../decline-voting-rights-collector.ts        | 47 +++++++++++
 .../collectors/jsonrpc/witness-collector.ts   |  4 +-
 .../factories/jsonrpc/factory-data.ts         |  8 +-
 src/chain-observers/filters/alarm-filter.ts   | 50 ++++++++++++
 .../providers/alarm-provider.ts               | 80 +++++++++++++++++++
 src/queen.ts                                  | 11 +++
 src/wax/index.ts                              | 15 ++++
 13 files changed, 302 insertions(+), 4 deletions(-)
 create mode 100644 src/chain-observers/classifiers/change-recovery-in-progress-classifier.ts
 create mode 100644 src/chain-observers/classifiers/decline-voting-rights-classifier.ts
 create mode 100644 src/chain-observers/collectors/jsonrpc/change-recovery-in-progress-collector.ts
 create mode 100644 src/chain-observers/collectors/jsonrpc/decline-voting-rights-collector.ts
 create mode 100644 src/chain-observers/filters/alarm-filter.ts
 create mode 100644 src/chain-observers/providers/alarm-provider.ts

diff --git a/src/chain-observers/classifiers/account-classifier.ts b/src/chain-observers/classifiers/account-classifier.ts
index 17d14c3..81e5a54 100644
--- a/src/chain-observers/classifiers/account-classifier.ts
+++ b/src/chain-observers/classifiers/account-classifier.ts
@@ -29,6 +29,8 @@ export interface IAccount {
   postingJsonMetadata: Record<string, any>;
   jsonMetadata: Record<string, any>;
   balance: IAccountBalance;
+  recoveryAccount: string;
+  governanceVoteExpiration?: Date;
 }
 
 export interface IAccountData {
diff --git a/src/chain-observers/classifiers/change-recovery-in-progress-classifier.ts b/src/chain-observers/classifiers/change-recovery-in-progress-classifier.ts
new file mode 100644
index 0000000..7fd32cc
--- /dev/null
+++ b/src/chain-observers/classifiers/change-recovery-in-progress-classifier.ts
@@ -0,0 +1,15 @@
+import { CollectorClassifierBase } from "./collector-classifier-base";
+
+export interface IAccountChangingRecovery {
+  accountToRecover: string;
+  recoveryAccount: string;
+  effectiveOn: Date;
+}
+
+export interface IChangeRecoveryInProgressData {
+  recoveringAccounts: Record<string, IAccountChangingRecovery>;
+}
+
+export class ChangeRecoveryInProgressClassifier extends CollectorClassifierBase {
+  public type!: IChangeRecoveryInProgressData;
+}
diff --git a/src/chain-observers/classifiers/decline-voting-rights-classifier.ts b/src/chain-observers/classifiers/decline-voting-rights-classifier.ts
new file mode 100644
index 0000000..60d33eb
--- /dev/null
+++ b/src/chain-observers/classifiers/decline-voting-rights-classifier.ts
@@ -0,0 +1,14 @@
+import { CollectorClassifierBase } from "./collector-classifier-base";
+
+export interface IDeclinedVotingRightsAccount {
+  account: string;
+  effectiveDate: Date;
+}
+
+export interface IDeclineVotingRightsAccountsData {
+  declineVotingRightsAccounts: Record<string, IDeclinedVotingRightsAccount>;
+}
+
+export class DeclineVotingRightsClassifier extends CollectorClassifierBase {
+  public type!: IDeclineVotingRightsAccountsData;
+}
diff --git a/src/chain-observers/classifiers/index.ts b/src/chain-observers/classifiers/index.ts
index 0301e7e..26a340e 100644
--- a/src/chain-observers/classifiers/index.ts
+++ b/src/chain-observers/classifiers/index.ts
@@ -7,3 +7,5 @@ export { ImpactedAccountClassifier } from "./impacted-account-classifier";
 export { OperationClassifier } from "./operation-classifier";
 export { FeedPriceClassifier } from "./feed-price-classifier";
 export { WitnessClassifier } from "./witness-classifier";
+export { ChangeRecoveryInProgressClassifier } from "./change-recovery-in-progress-classifier";
+export { DeclineVotingRightsClassifier } from "./decline-voting-rights-classifier";
diff --git a/src/chain-observers/collectors/jsonrpc/account-collector.ts b/src/chain-observers/collectors/jsonrpc/account-collector.ts
index 316d98c..f6ed8e1 100644
--- a/src/chain-observers/collectors/jsonrpc/account-collector.ts
+++ b/src/chain-observers/collectors/jsonrpc/account-collector.ts
@@ -39,9 +39,16 @@ export class AccountCollector extends CollectorBase {
 
       const { accounts: apiAccounts } = await this.worker.chain!.api.database_api.find_accounts({ accounts: chunk });
 
-      for(const account of apiAccounts)
+      for(const account of apiAccounts) {
+        let governanceVoteExpiration: Date | undefined = new Date(`${account.governance_vote_expiration_ts}Z`);
+
+        if (governanceVoteExpiration.getTime() <= 0) // Null time
+          governanceVoteExpiration = undefined;
+
         accounts[account.name] = {
           name: account.name,
+          recoveryAccount: account.recovery_account,
+          governanceVoteExpiration,
           postingJsonMetadata: tryParseJson(account.posting_json_metadata),
           jsonMetadata: tryParseJson(account.json_metadata),
           balance: {
@@ -89,6 +96,7 @@ export class AccountCollector extends CollectorBase {
             }
           }
         };
+      }
     }
 
     return {
diff --git a/src/chain-observers/collectors/jsonrpc/change-recovery-in-progress-collector.ts b/src/chain-observers/collectors/jsonrpc/change-recovery-in-progress-collector.ts
new file mode 100644
index 0000000..9b4874f
--- /dev/null
+++ b/src/chain-observers/collectors/jsonrpc/change-recovery-in-progress-collector.ts
@@ -0,0 +1,48 @@
+import { IAccountChangingRecovery } from "../../classifiers/change-recovery-in-progress-classifier";
+import { DataEvaluationContext } from "../../factories/data-evaluation-context";
+import { CollectorBase, TAvailableClassifiers } from "../collector-base";
+
+export interface IChangeRecoveryCollectorOptions {
+  changeRecoveryAccount: string;
+}
+
+const MAX_CHANGE_RECOVERY_GET_LIMIT = 100;
+
+export class ChangeRecoveryInProgressCollector extends CollectorBase {
+  private readonly changeRecoveryAccounts: Record<string, number> = {};
+
+  protected pushOptions(data: IChangeRecoveryCollectorOptions): void {
+    this.changeRecoveryAccounts[data.changeRecoveryAccount] = (this.changeRecoveryAccounts[data.changeRecoveryAccount] || 0) + 1;
+  }
+
+  protected popOptions(data: IChangeRecoveryCollectorOptions): void {
+    this.changeRecoveryAccounts[data.changeRecoveryAccount] = (this.changeRecoveryAccounts[data.changeRecoveryAccount] || 1) - 1;
+
+    if (this.changeRecoveryAccounts[data.changeRecoveryAccount] === 0)
+      delete this.changeRecoveryAccounts[data.changeRecoveryAccount];
+  }
+
+  public async fetchData(_: DataEvaluationContext) {
+    const retrieveChangeRecoveryAccounts: Record<string, IAccountChangingRecovery> = {};
+
+    const recoveryAccounts = Object.keys(this.changeRecoveryAccounts);
+    for (let i = 0; i < recoveryAccounts.length; i += MAX_CHANGE_RECOVERY_GET_LIMIT) {
+      const chunk = recoveryAccounts.slice(i, i + MAX_CHANGE_RECOVERY_GET_LIMIT);
+
+      const { requests } = await this.worker.chain!.api.database_api.find_change_recovery_account_requests({ accounts: chunk });
+
+      for(const request of requests)
+        retrieveChangeRecoveryAccounts[request.account_to_recover] = {
+          accountToRecover: request.account_to_recover,
+          recoveryAccount: request.recovery_account,
+          effectiveOn: new Date(`${request.effective_on}Z`)
+        };
+    }
+
+    return {
+      ChangeRecoveryInProgressClassifier: {
+        recoveringAccounts: retrieveChangeRecoveryAccounts
+      }
+    } satisfies Partial<TAvailableClassifiers>;
+  };
+}
diff --git a/src/chain-observers/collectors/jsonrpc/decline-voting-rights-collector.ts b/src/chain-observers/collectors/jsonrpc/decline-voting-rights-collector.ts
new file mode 100644
index 0000000..9720b88
--- /dev/null
+++ b/src/chain-observers/collectors/jsonrpc/decline-voting-rights-collector.ts
@@ -0,0 +1,47 @@
+import { IDeclinedVotingRightsAccount } from "../../classifiers/decline-voting-rights-classifier";
+import { DataEvaluationContext } from "../../factories/data-evaluation-context";
+import { CollectorBase, TAvailableClassifiers } from "../collector-base";
+
+export interface IDeclineVotingRightsCollectorOptions {
+  declineVotingRightsAccount: string;
+}
+
+const MAX_DECLINED_VOTING_RIGHTS_GET_LIMIT = 100;
+
+export class DeclineVotingRightsCollector extends CollectorBase {
+  private readonly declineVotingRightsAccounts: Record<string, number> = {};
+
+  protected pushOptions(data: IDeclineVotingRightsCollectorOptions): void {
+    this.declineVotingRightsAccounts[data.declineVotingRightsAccount] = (this.declineVotingRightsAccounts[data.declineVotingRightsAccount] || 0) + 1;
+  }
+
+  protected popOptions(data: IDeclineVotingRightsCollectorOptions): void {
+    this.declineVotingRightsAccounts[data.declineVotingRightsAccount] = (this.declineVotingRightsAccounts[data.declineVotingRightsAccount] || 1) - 1;
+
+    if (this.declineVotingRightsAccounts[data.declineVotingRightsAccount] === 0)
+      delete this.declineVotingRightsAccounts[data.declineVotingRightsAccount];
+  }
+
+  public async fetchData(_: DataEvaluationContext) {
+    const declineVotingRightsAccounts: Record<string, IDeclinedVotingRightsAccount> = {};
+
+    const recoveryAccounts = Object.keys(this.declineVotingRightsAccounts);
+    for (let i = 0; i < recoveryAccounts.length; i += MAX_DECLINED_VOTING_RIGHTS_GET_LIMIT) {
+      const chunk = recoveryAccounts.slice(i, i + MAX_DECLINED_VOTING_RIGHTS_GET_LIMIT);
+
+      const { requests } = await this.worker.chain!.api.database_api.find_decline_voting_rights_requests({ accounts: chunk });
+
+      for(const request of requests)
+        declineVotingRightsAccounts[request.account] = {
+          account: request.account,
+          effectiveDate: new Date(`${request.effective_date}Z`)
+        };
+    }
+
+    return {
+      DeclineVotingRightsClassifier: {
+        declineVotingRightsAccounts
+      }
+    } satisfies Partial<TAvailableClassifiers>;
+  };
+}
diff --git a/src/chain-observers/collectors/jsonrpc/witness-collector.ts b/src/chain-observers/collectors/jsonrpc/witness-collector.ts
index 39f3409..f1b50ef 100644
--- a/src/chain-observers/collectors/jsonrpc/witness-collector.ts
+++ b/src/chain-observers/collectors/jsonrpc/witness-collector.ts
@@ -32,8 +32,8 @@ export class WitnessCollector extends CollectorBase {
       const { witnesses: owners } = await this.worker.chain!.api.database_api.find_witnesses({ owners: chunk });
 
       for(const account of owners)
-        witnessNames[account.owner] = {
-          name: account.owner,
+        witnesses[account.owner] = {
+          owner: account.owner,
           runningVersion: account.running_version,
           totalMissedBlocks: account.total_missed,
           lastConfirmedBlockNum: account.last_confirmed_block_num
diff --git a/src/chain-observers/factories/jsonrpc/factory-data.ts b/src/chain-observers/factories/jsonrpc/factory-data.ts
index 3662dfb..1b5bf65 100644
--- a/src/chain-observers/factories/jsonrpc/factory-data.ts
+++ b/src/chain-observers/factories/jsonrpc/factory-data.ts
@@ -1,6 +1,8 @@
 import type { WorkerBee } from "../../../bot";
 import {
   AccountClassifier, BlockClassifier, BlockHeaderClassifier,
+  ChangeRecoveryInProgressClassifier,
+  DeclineVotingRightsClassifier,
   DynamicGlobalPropertiesClassifier, FeedPriceClassifier, ImpactedAccountClassifier, OperationClassifier, RcAccountClassifier,
   WitnessClassifier
 } from "../../classifiers";
@@ -11,6 +13,8 @@ import { ImpactedAccountCollector } from "../../collectors/common/impacted-accou
 import { OperationCollector } from "../../collectors/common/operation-collector";
 import { AccountCollector } from "../../collectors/jsonrpc/account-collector";
 import { BlockCollector } from "../../collectors/jsonrpc/block-collector";
+import { ChangeRecoveryInProgressCollector } from "../../collectors/jsonrpc/change-recovery-in-progress-collector";
+import { DeclineVotingRightsCollector } from "../../collectors/jsonrpc/decline-voting-rights-collector";
 import { DynamicGlobalPropertiesCollector } from "../../collectors/jsonrpc/dynamic-global-properties-collector";
 import { FeedPriceCollector } from "../../collectors/jsonrpc/feed-price-collector";
 import { RcAccountCollector } from "../../collectors/jsonrpc/rc-account-collector";
@@ -25,5 +29,7 @@ export const JsonRpcFactoryData: (worker: WorkerBee) => Array<[IEvaluationContex
   [ImpactedAccountClassifier, new ImpactedAccountCollector(worker)],
   [OperationClassifier, new OperationCollector(worker)],
   [FeedPriceClassifier, new FeedPriceCollector(worker)],
-  [WitnessClassifier, new WitnessCollector(worker)]
+  [WitnessClassifier, new WitnessCollector(worker)],
+  [ChangeRecoveryInProgressClassifier, new ChangeRecoveryInProgressCollector(worker)],
+  [DeclineVotingRightsClassifier, new DeclineVotingRightsCollector(worker)]
 ]);
diff --git a/src/chain-observers/filters/alarm-filter.ts b/src/chain-observers/filters/alarm-filter.ts
new file mode 100644
index 0000000..19efd4d
--- /dev/null
+++ b/src/chain-observers/filters/alarm-filter.ts
@@ -0,0 +1,50 @@
+import type { WorkerBee } from "../../bot";
+import { AccountClassifier, ChangeRecoveryInProgressClassifier, DeclineVotingRightsClassifier } from "../classifiers";
+import type { TRegisterEvaluationContext } from "../classifiers/collector-classifier-base";
+import type { DataEvaluationContext } from "../factories/data-evaluation-context";
+import { FilterBase } from "./filter-base";
+
+export const STEEM_ACCOUNT_NAME = "steem";
+export const ONE_MONTH_MS = 1000 * 60 * 60 * 24 * 31;
+
+export class AlarmFilter extends FilterBase {
+  public constructor(
+    worker: WorkerBee,
+    private readonly account: string
+  ) {
+    super(worker);
+  }
+
+  public usedContexts(): Array<TRegisterEvaluationContext> {
+    return [
+      AccountClassifier.forOptions({ account: this.account }),
+      ChangeRecoveryInProgressClassifier.forOptions({ changeRecoveryAccount: this.account }),
+      DeclineVotingRightsClassifier.forOptions({ declineVotingRightsAccount: this.account })
+    ];
+  }
+
+  public async match(data: DataEvaluationContext): Promise<boolean> {
+    const { accounts } = await data.get(AccountClassifier);
+
+    if (accounts[this.account].recoveryAccount === STEEM_ACCOUNT_NAME)
+      return true;
+
+    if (accounts[this.account].governanceVoteExpiration === undefined)
+      return true;
+
+    if (accounts[this.account].governanceVoteExpiration!.getTime() < (Date.now() + ONE_MONTH_MS))
+      return true;
+
+    const { recoveringAccounts } = await data.get(ChangeRecoveryInProgressClassifier);
+
+    if (recoveringAccounts[this.account])
+      return true;
+
+    const { declineVotingRightsAccounts } = await data.get(DeclineVotingRightsClassifier);
+
+    if (declineVotingRightsAccounts[this.account])
+      return true;
+
+    return false;
+  }
+}
diff --git a/src/chain-observers/providers/alarm-provider.ts b/src/chain-observers/providers/alarm-provider.ts
new file mode 100644
index 0000000..ecb6a8f
--- /dev/null
+++ b/src/chain-observers/providers/alarm-provider.ts
@@ -0,0 +1,80 @@
+import { TAccountName } from "@hiveio/wax";
+import { AccountClassifier, ChangeRecoveryInProgressClassifier, DeclineVotingRightsClassifier } from "../classifiers";
+import { TRegisterEvaluationContext } from "../classifiers/collector-classifier-base";
+import { DataEvaluationContext } from "../factories/data-evaluation-context";
+import { ONE_MONTH_MS, STEEM_ACCOUNT_NAME } from "../filters/alarm-filter";
+import { ProviderBase } from "./provider-base";
+
+export enum EAlarmType {
+  LEGACY_RECOVERY_ACCOUNT_SET,
+  GOVERNANCE_VOTE_EXPIRATION_SOON,
+  GOVERNANCE_VOTE_EXPIRED,
+  RECOVERY_ACCOUNT_IS_CHANGING,
+  DECLINING_VOTING_RIGHTS
+}
+
+export type TAlarmAccounts<TAccounts extends Array<TAccountName>> = {
+  [K in TAccounts[number]]: EAlarmType[];
+};
+
+export interface IAlarmAccountsData<TAccounts extends Array<TAccountName>> {
+  alarmsPerAccount: TAlarmAccounts<TAccounts>;
+};
+
+export class AlarmProvider<TAccounts extends Array<TAccountName> = Array<TAccountName>> extends ProviderBase {
+  public constructor(
+    private readonly accounts: TAccounts
+  ) {
+    super();
+  }
+
+  public usedContexts(): Array<TRegisterEvaluationContext> {
+    return [
+      ...this.accounts.map(account => AccountClassifier.forOptions({
+        account
+      })),
+      ...this.accounts.map(account => ChangeRecoveryInProgressClassifier.forOptions({
+        changeRecoveryAccount: account
+      })),
+      ...this.accounts.map(account => DeclineVotingRightsClassifier.forOptions({
+        declineVotingRightsAccount: account
+      }))
+    ];
+  }
+
+  public async provide(data: DataEvaluationContext): Promise<IAlarmAccountsData<TAccounts>> {
+    const result: IAlarmAccountsData<TAccounts> = {
+      alarmsPerAccount: this.accounts.reduce((prev, curr) => {
+        prev[curr]  = [];
+
+        return prev;
+      }, {} as TAlarmAccounts<TAccounts>)
+    };
+
+    const { accounts } = await data.get(AccountClassifier);
+    for(const account of this.accounts) {
+      if (accounts[account].recoveryAccount === STEEM_ACCOUNT_NAME)
+        result.alarmsPerAccount[account].push(EAlarmType.LEGACY_RECOVERY_ACCOUNT_SET);
+      if (accounts[account].governanceVoteExpiration === undefined)
+        result.alarmsPerAccount[account].push(EAlarmType.GOVERNANCE_VOTE_EXPIRED);
+      if (accounts[account].governanceVoteExpiration!.getTime() < (Date.now() + ONE_MONTH_MS))
+        result.alarmsPerAccount[account].push(EAlarmType.GOVERNANCE_VOTE_EXPIRATION_SOON);
+    }
+
+    const { recoveringAccounts } = await data.get(ChangeRecoveryInProgressClassifier);
+
+    for(const account of this.accounts)
+      if (recoveringAccounts[account])
+        result.alarmsPerAccount[account].push(EAlarmType.RECOVERY_ACCOUNT_IS_CHANGING);
+
+
+    const { declineVotingRightsAccounts } = await data.get(DeclineVotingRightsClassifier);
+
+    for(const account of this.accounts)
+      if (declineVotingRightsAccounts[account])
+        result.alarmsPerAccount[account].push(EAlarmType.DECLINING_VOTING_RIGHTS);
+
+
+    return result;
+  }
+}
diff --git a/src/queen.ts b/src/queen.ts
index 7b1c8dc..b6dabb1 100644
--- a/src/queen.ts
+++ b/src/queen.ts
@@ -4,6 +4,7 @@ import { WorkerBee } from "./bot";
 import { AccountCreatedFilter } from "./chain-observers/filters/account-created-filter";
 import { AccountFullManabarFilter } from "./chain-observers/filters/account-full-manabar-filter";
 import { AccountMetadataChangeFilter } from "./chain-observers/filters/account-metadata-change-filter";
+import { AlarmFilter } from "./chain-observers/filters/alarm-filter";
 import { BalanceChangeFilter } from "./chain-observers/filters/balance-change-filter";
 import { BlockNumberFilter } from "./chain-observers/filters/block-filter";
 import { LogicalAndFilter, LogicalOrFilter } from "./chain-observers/filters/composite-filter";
@@ -24,6 +25,7 @@ import { VoteFilter } from "./chain-observers/filters/vote-filter";
 import { WhaleAlertFilter } from "./chain-observers/filters/whale-alert-filter";
 import { WitnessMissedBlocksFilter } from "./chain-observers/filters/witness-miss-block-filter";
 import { AccountProvider } from "./chain-observers/providers/account-provider";
+import { AlarmProvider } from "./chain-observers/providers/alarm-provider";
 import { BlockHeaderProvider } from "./chain-observers/providers/block-header-provider";
 import { BlockProvider } from "./chain-observers/providers/block-provider";
 import { ExchangeTransferProvider } from "./chain-observers/providers/exchange-transfer-provider";
@@ -180,6 +182,15 @@ export class QueenBee<TPreviousSubscriberData extends object = {}> {
     return this;
   }
 
+  public onAlarm<
+    TAccount extends TAccountName
+  >(watchAccount: TAccount): QueenBee<TPreviousSubscriberData & Awaited<ReturnType<AlarmProvider<[TAccount]>["provide"]>>> {
+    this.operands.push(new AlarmFilter(this.worker, watchAccount));
+    this.providers.push(new AlarmProvider<[TAccount]>([watchAccount]));
+
+    return this;
+  }
+
   public onImpactedAccount(account: TAccountName): QueenBee<TPreviousSubscriberData> {
     this.operands.push(new ImpactedAccountFilter(this.worker, account));
 
diff --git a/src/wax/index.ts b/src/wax/index.ts
index d886a22..795b6a7 100644
--- a/src/wax/index.ts
+++ b/src/wax/index.ts
@@ -22,6 +22,21 @@ export type WaxExtendTypes = {
         // ...
       }>; };
     };
+    find_decline_voting_rights_requests: {
+      params: { accounts: string[]; };
+      result: { requests: Array<{
+        account: string;
+        effective_date: string;
+      }>; };
+    };
+    find_change_recovery_account_requests: {
+      params: { accounts: string[]; };
+      result: { requests: Array<{
+        account_to_recover: string;
+        recovery_account: string;
+        effective_on: string;
+      }>; };
+    };
   }
 };
 
-- 
GitLab