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