diff --git a/src/chain-observers/classifiers/account-classifier.ts b/src/chain-observers/classifiers/account-classifier.ts index 17d14c3cfe606f418d862b0c6b0de147db9574b3..81e5a5480ea4fa9e1cc746dcc6f4b43b894ec66a 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 0000000000000000000000000000000000000000..7fd32cc5b1d047fc6097019afe42c8260d3a7304 --- /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 0000000000000000000000000000000000000000..60d33eba197780c1e235053046cc31fbe8ae799f --- /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 0301e7ecf2ba640e3ded8bf82fbd88cfc9e93d92..26a340e13236e16726f7904458798649c78922a3 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 316d98c30296e309cf79e7552b0156552b4a50f2..f6ed8e14731c9b603fde997d7bfed15e80641222 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 0000000000000000000000000000000000000000..9b4874f5b0d5971b7318750c5e91065e87adb0d2 --- /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 0000000000000000000000000000000000000000..9720b88ab274b29ed2c2969147435bf8ea3b05f0 --- /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 39f34098dd19108d50ea23c8e598e9137f8a0ea6..f1b50ef41018e431ea400845ac13588b847480ce 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 3662dfb460c0d1575122976ea5e4024a72d06435..1b5bf654860cf38cb588a30ef27ba7137bfc96c6 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 0000000000000000000000000000000000000000..19efd4db7501b5f83d55bbd6e52be6497de727fa --- /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 0000000000000000000000000000000000000000..ecb6a8f57be126a5bc40e4632e2c7c552929d4f2 --- /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 7b1c8dcffebf6c20aed4f3629fa3aae06e011c5b..b6dabb1b0875f3fedde79f22ff6f67ebf7aa996d 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 d886a225307e60d9bc635f8ce45f7410426fcbe6..795b6a731dd391a933f138899d1449b7ed85e7c0 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; + }>; }; + }; } };