diff --git a/apps/agent/config.example.yml b/apps/agent/config.example.yml index e61103bf..a52c3576 100644 --- a/apps/agent/config.example.yml +++ b/apps/agent/config.example.yml @@ -16,6 +16,7 @@ protocolProvider: eboRequestCreator: "0x1234567890123456789012345678901234567890" # EBO Request Creator contract bondEscalationModule: "0x1234567890123456789012345678901234567890" # Bond Escalation Module contract horizonAccountingExtension: "0x1234567890123456789012345678901234567890" # Accounting extension contract + horizonStaking: "0x1234567890123456789012345678901234567890" # Horizon Staking contract accessControl: serviceProviderAddress: "0x1234567890123456789012345678901234567890" # Service Provider Address diff --git a/apps/agent/config.tenderly.yml b/apps/agent/config.tenderly.yml index d8a082f2..666b7e58 100644 --- a/apps/agent/config.tenderly.yml +++ b/apps/agent/config.tenderly.yml @@ -16,6 +16,7 @@ protocolProvider: eboRequestCreator: "0xa13318684281a820304C164427396385C306d870" bondEscalationModule: "0x52d7728fE87826FfF51b21b303e2FF7cB04F6Aec" horizonAccountingExtension: "0xbDAB27D1903da4e18B0D1BE873E18924514E52eC" + horizonStaking: "0x3F53F9f9a5d7F36dCC869f8D2F227499c411c0cf" blockNumberService: blockmetaConfig: diff --git a/apps/agent/src/config/schemas.ts b/apps/agent/src/config/schemas.ts index dc83d15b..84e9430a 100644 --- a/apps/agent/src/config/schemas.ts +++ b/apps/agent/src/config/schemas.ts @@ -33,7 +33,7 @@ export const envSchema = z.object({ DISCORD_WEBHOOK: z.string().optional(), }); -const addressSchema = z.string().refine((address) => isAddress(address)); +export const addressSchema = z.string().refine((address) => isAddress(address)); const rpcConfigSchema = z.object({ chainId: z @@ -64,6 +64,7 @@ const protocolProviderConfigSchema = z.object({ eboRequestCreator: addressSchema, bondEscalationModule: addressSchema, horizonAccountingExtension: addressSchema, + horizonStaking: addressSchema, }), accessControl: accessControlSchema.optional(), }); diff --git a/apps/scripts/.env.example b/apps/scripts/.env.example index aa918b0c..ee38ee16 100644 --- a/apps/scripts/.env.example +++ b/apps/scripts/.env.example @@ -4,6 +4,6 @@ RPC_URLS_L2=["https://l2.rpc.url"] TRANSACTION_RECEIPT_CONFIRMATIONS=1 TIMEOUT=30000 RETRY_INTERVAL=1000 -CONTRACTS_ADDRESSES={"l1ChainId":"eip155:1","l2ChainId":"eip155:42161","oracle":"0x...","epochManager":"0x...","bondEscalationModule":"0x...","horizonAccountingExtension":"0x..."} +CONTRACTS_ADDRESSES={"l1ChainId":"eip155:1","l2ChainId":"eip155:42161","oracle":"0x...","epochManager":"0x...","bondEscalationModule":"0x...","horizonAccountingExtension":"0x...","horizonStaking":"0x..."} BONDED_RESPONSE_MODULE_ADDRESS=0x... BOND_ESCALATION_MODULE_ADDRESS=0x... \ No newline at end of file diff --git a/apps/scripts/test/setup.ts b/apps/scripts/test/setup.ts index a9a243c0..0996b35a 100644 --- a/apps/scripts/test/setup.ts +++ b/apps/scripts/test/setup.ts @@ -16,6 +16,7 @@ vi.stubEnv( epochManager: "0x0000000000000000000000000000000000000002", bondEscalationModule: "0x0000000000000000000000000000000000000003", horizonAccountingExtension: "0x0000000000000000000000000000000000000004", + horizonStaking: "0x0000000000000000000000000000000000000005", }), ); vi.stubEnv("BONDED_RESPONSE_MODULE_ADDRESS", "0xBondedResponseModule"); diff --git a/apps/scripts/utilities/approveAccountingModules.ts b/apps/scripts/utilities/approveAccountingModules.ts index 1f306c18..88efa45d 100644 --- a/apps/scripts/utilities/approveAccountingModules.ts +++ b/apps/scripts/utilities/approveAccountingModules.ts @@ -20,11 +20,9 @@ const stringToJSONSchema = z.string().transform((str, ctx): Record isHex(val), { - message: "Must be a valid Hex string", -}); +const addressSchema = z.string().refine((address) => isAddress(address)); /** * Defines the schema for CONTRACTS_ADDRESSES based on expected structure. @@ -36,14 +34,17 @@ const contractsAddressesSchema = z.object({ l2ChainId: z.string().refine((val): val is Caip2ChainId => val.includes(":"), { message: "l2ChainId must be in the format 'namespace:chainId' (e.g., 'eip155:42161')", }), - oracle: hexSchema, - epochManager: hexSchema, - bondEscalationModule: hexSchema, - horizonAccountingExtension: hexSchema + oracle: addressSchema, + epochManager: addressSchema, + bondEscalationModule: addressSchema, + horizonAccountingExtension: addressSchema .optional() .default("0x0000000000000000000000000000000000000000"), + horizonStaking: addressSchema, // Default to a zero address because it's not required for the script but required for ProtocolProvider - eboRequestCreator: hexSchema.optional().default("0x0000000000000000000000000000000000000000"), + eboRequestCreator: addressSchema + .optional() + .default("0x0000000000000000000000000000000000000000"), }); /** diff --git a/packages/automated-dispute/src/abis/horizonStaking.ts b/packages/automated-dispute/src/abis/horizonStaking.ts new file mode 100644 index 00000000..47e3b033 --- /dev/null +++ b/packages/automated-dispute/src/abis/horizonStaking.ts @@ -0,0 +1,93 @@ +export const horizonStakingAbi = [ + { + type: "function", + name: "addToProvision", + inputs: [ + { name: "serviceProvider", type: "address", internalType: "address" }, + { name: "verifier", type: "address", internalType: "address" }, + { name: "tokens", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "getProvision", + inputs: [ + { name: "serviceProvider", type: "address", internalType: "address" }, + { name: "verifier", type: "address", internalType: "address" }, + ], + outputs: [ + { + name: "", + type: "tuple", + internalType: "struct IHorizonStaking.Provision", + components: [ + { name: "tokens", type: "uint256", internalType: "uint256" }, + { name: "tokensThawing", type: "uint256", internalType: "uint256" }, + { name: "sharesThawing", type: "uint256", internalType: "uint256" }, + { name: "maxVerifierCut", type: "uint32", internalType: "uint32" }, + { name: "thawingPeriod", type: "uint64", internalType: "uint64" }, + { name: "createdAt", type: "uint64", internalType: "uint64" }, + { name: "maxVerifierCutPending", type: "uint32", internalType: "uint32" }, + { name: "thawingPeriodPending", type: "uint64", internalType: "uint64" }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "isAuthorized", + inputs: [ + { name: "serviceProvider", type: "address", internalType: "address" }, + { name: "verifier", type: "address", internalType: "address" }, + { name: "operator", type: "address", internalType: "address" }, + ], + outputs: [{ name: "", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "provision", + inputs: [ + { name: "serviceProvider", type: "address", internalType: "address" }, + { name: "verifier", type: "address", internalType: "address" }, + { name: "tokens", type: "uint256", internalType: "uint256" }, + { name: "maxVerifierCut", type: "uint32", internalType: "uint32" }, + { name: "thawingPeriod", type: "uint64", internalType: "uint64" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "slash", + inputs: [ + { name: "serviceProvider", type: "address", internalType: "address" }, + { name: "tokens", type: "uint256", internalType: "uint256" }, + { name: "tokensVerifier", type: "uint256", internalType: "uint256" }, + { name: "verifierDestination", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "stake", + inputs: [{ name: "tokens", type: "uint256", internalType: "uint256" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "thaw", + inputs: [ + { name: "serviceProvider", type: "address", internalType: "address" }, + { name: "verifier", type: "address", internalType: "address" }, + { name: "tokens", type: "uint256", internalType: "uint256" }, + ], + outputs: [{ name: "", type: "bytes32", internalType: "bytes32" }], + stateMutability: "nonpayable", + }, +] as const; diff --git a/packages/automated-dispute/src/abis/index.ts b/packages/automated-dispute/src/abis/index.ts index f5c80b63..37c77612 100644 --- a/packages/automated-dispute/src/abis/index.ts +++ b/packages/automated-dispute/src/abis/index.ts @@ -3,3 +3,4 @@ export * from "./epochManager.js"; export * from "./eboRequestCreator.js"; export * from "./bondEscalationModule.js"; export * from "./horizonAccountingExtension.js"; +export * from "./horizonStaking.js"; diff --git a/packages/automated-dispute/src/constants.ts b/packages/automated-dispute/src/constants.ts index e2290c52..7e900059 100644 --- a/packages/automated-dispute/src/constants.ts +++ b/packages/automated-dispute/src/constants.ts @@ -4,4 +4,5 @@ export const ProtocolContractsNames = [ "eboRequestCreator", "bondEscalationModule", "horizonAccountingExtension", + "horizonStaking", ] as const; diff --git a/packages/automated-dispute/src/interfaces/protocolProvider.ts b/packages/automated-dispute/src/interfaces/protocolProvider.ts index bcf25ab5..a0f5b2b5 100644 --- a/packages/automated-dispute/src/interfaces/protocolProvider.ts +++ b/packages/automated-dispute/src/interfaces/protocolProvider.ts @@ -80,6 +80,15 @@ export interface IReadProvider { * @returns A Promise that resolves to the BondEscalation data. */ getEscalation(requestId: RequestId): Promise; + + /** + * @notice Check if an operator is authorized for the caller on a specific verifier/data service. + * @param serviceProvider The service provider on behalf of whom they're claiming to act + * @param verifier The verifier/data service on which they're claiming to act + * @param operator The address to check for auth + * @return Whether the operator is authorized or not + */ + isAuthorized(serviceProvider: Address, verifier: Address, operator: Address): Promise; } /** @@ -90,11 +99,11 @@ export interface IWriteProvider { * Creates a request on the EBO Request Creator contract. * * @param epoch The epoch for which the request is being created. - * @param chains An array of chain identifiers where the request should be created. + * @param chain A chain identifier where the request should be created. * @throws Will throw an error if the chains array is empty or if the transaction fails. * @returns A promise that resolves when the request is successfully created. */ - createRequest(epoch: bigint, chains: Caip2ChainId): Promise; + createRequest(epoch: bigint, chain: Caip2ChainId): Promise; /** * Proposes a response to a request. diff --git a/packages/automated-dispute/src/providers/protocolProvider.ts b/packages/automated-dispute/src/providers/protocolProvider.ts index 9daa1962..ad75ab5f 100644 --- a/packages/automated-dispute/src/providers/protocolProvider.ts +++ b/packages/automated-dispute/src/providers/protocolProvider.ts @@ -41,6 +41,7 @@ import { eboRequestCreatorAbi, epochManagerAbi, horizonAccountingExtensionAbi, + horizonStakingAbi, oracleAbi, } from "../abis/index.js"; import { @@ -121,6 +122,12 @@ export class ProtocolProvider implements IProtocolProvider { Address >; + private horizonStakingContract: GetContractReturnType< + typeof horizonStakingAbi, + typeof this.l2ReadClient, + Address + >; + /** * Creates a new ProtocolProvider instance * @param rpcConfig The configuration for the serviceProviderAddress and RPC connections, including URLs, timeout, retry interval, and transaction receipt confirmations @@ -177,11 +184,7 @@ export class ProtocolProvider implements IProtocolProvider { public: this.l2ReadClient, wallet: this.l2WriteClient, }, - }) as GetContractReturnType< - typeof bondEscalationModuleAbi, - typeof this.l2WriteClient, - Address - >; + }); this.horizonAccountingExtensionContract = getContract({ address: contracts.horizonAccountingExtension, @@ -190,11 +193,13 @@ export class ProtocolProvider implements IProtocolProvider { public: this.l2ReadClient, wallet: this.l2WriteClient, }, - }) as GetContractReturnType< - typeof horizonAccountingExtensionAbi, - typeof this.l2WriteClient, - Address - >; + }); + + this.horizonStakingContract = getContract({ + address: contracts.horizonStaking, + abi: horizonStakingAbi, + client: this.l2ReadClient, + }); } public write: IWriteProvider = { @@ -217,6 +222,7 @@ export class ProtocolProvider implements IProtocolProvider { getAccountingModuleAddress: this.getAccountingModuleAddress.bind(this), getApprovedModules: this.getApprovedModules.bind(this), getEscalation: this.getEscalation.bind(this), + isAuthorized: this.isAuthorized.bind(this), }; /** @@ -852,11 +858,7 @@ export class ProtocolProvider implements IProtocolProvider { async getApprovedModules(user: Address): Promise { const bondAddress = user ?? this.getAccountAddress(); - const modules = await this.horizonAccountingExtensionContract.read.approvedModules([ - bondAddress, - ]); - - return modules; + return await this.horizonAccountingExtensionContract.read.approvedModules([bondAddress]); } // TODO: waiting for ChainId to be merged for _chains parameter @@ -865,16 +867,16 @@ export class ProtocolProvider implements IProtocolProvider { * and then executing it if the simulation is successful. * * @param {bigint} epoch - The epoch for which the request is being created. - * @param {Caip2ChainId} chain - An array of chain identifiers for which the request should be created. + * @param {Caip2ChainId} chain - A chain identifier for which the request should be created. * @throws {Error} Throws an error if the chains array is empty or if the transaction fails. * @returns {Promise} A promise that resolves when the request is successfully created. */ - async createRequest(epoch: bigint, chains: Caip2ChainId): Promise { + async createRequest(epoch: bigint, chain: Caip2ChainId): Promise { const { request: simulatedRequest } = await this.l2ReadClient.simulateContract({ address: this.eboRequestCreatorContract.address, abi: eboRequestCreatorAbi, functionName: "createRequest", - args: [epoch, chains], + args: [epoch, chain], account: this.l2WriteClient.account, }); @@ -1206,13 +1208,31 @@ export class ProtocolProvider implements IProtocolProvider { async getEscalation(requestId: RequestId): Promise { const result = await this.bondEscalationContract.read.getEscalation([requestId]); - const bondEscalation: BondEscalation = { + return { disputeId: result.disputeId, status: ProphetCodec.decodeBondEscalationStatus(result.status), amountOfPledgesForDispute: result.amountOfPledgesForDispute, amountOfPledgesAgainstDispute: result.amountOfPledgesAgainstDispute, }; + } - return bondEscalation; + /** + * Checks if an operator is authorized for a given service provider and verifier. + * + * @param serviceProvider - The service provider address. + * @param verifier - The verifier address. + * @param operator - The operator address. + * @returns A promise that resolves to a boolean indicating authorization status. + */ + async isAuthorized( + serviceProvider: Address, + verifier: Address, + operator: Address, + ): Promise { + return await this.horizonStakingContract.read.isAuthorized([ + serviceProvider, + verifier, + operator, + ]); } } diff --git a/packages/automated-dispute/src/services/eboProcessor.ts b/packages/automated-dispute/src/services/eboProcessor.ts index 0fe31603..4c980f28 100644 --- a/packages/automated-dispute/src/services/eboProcessor.ts +++ b/packages/automated-dispute/src/services/eboProcessor.ts @@ -61,6 +61,22 @@ export class EboProcessor { await this.checkAllModulesApproved(); + const serviceProvider = this.protocolProvider.getServiceProviderAddress(); + const verifier = this.protocolProvider.getAccountingModuleAddress(); + const operator = this.protocolProvider.getAccountAddress(); + + const isAuth = await this.protocolProvider.isAuthorized( + serviceProvider, + verifier, + operator, + ); + + if (!isAuth) { + const errorMessage = `Authorization required: Operator ${operator} is not authorized for service provider ${serviceProvider}.`; + this.logger.error(errorMessage); + process.exit(1); + } + await this.sync(); // Bootstrapping this.eventsInterval = setInterval(async () => { diff --git a/packages/automated-dispute/tests/mocks/eboActor.mocks.ts b/packages/automated-dispute/tests/mocks/eboActor.mocks.ts index d9b6e55c..fee57717 100644 --- a/packages/automated-dispute/tests/mocks/eboActor.mocks.ts +++ b/packages/automated-dispute/tests/mocks/eboActor.mocks.ts @@ -4,7 +4,7 @@ import { Mutex } from "async-mutex"; import { Block, pad } from "viem"; import { vi } from "vitest"; -import { ProtocolProvider } from "../../src/providers/index.js"; +import { ProtocolProvider } from "../../src/index.js"; import { EboActor, EboMemoryRegistry, ProphetCodec } from "../../src/services/index.js"; import { Dispute, diff --git a/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts b/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts index 47fba6cf..a2577cc1 100644 --- a/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts +++ b/packages/automated-dispute/tests/mocks/eboProcessor.mocks.ts @@ -2,9 +2,12 @@ import { BlockNumberService } from "@ebo-agent/blocknumber"; import { Caip2ChainId, ILogger, NotificationService } from "@ebo-agent/shared"; import { vi } from "vitest"; -import { ProtocolProvider } from "../../src/providers/index.js"; -import { EboActorsManager, EboProcessor } from "../../src/services/index.js"; -import { AccountingModules } from "../../src/types/prophet.js"; +import { + AccountingModules, + EboActorsManager, + EboProcessor, + ProtocolProvider, +} from "../../src/index.js"; import { DEFAULT_MOCKED_PROTOCOL_CONTRACTS, mockedPrivateKey, diff --git a/packages/automated-dispute/tests/services/eboActor/fixtures.ts b/packages/automated-dispute/tests/services/eboActor/fixtures.ts index be3ea45a..7874afb7 100644 --- a/packages/automated-dispute/tests/services/eboActor/fixtures.ts +++ b/packages/automated-dispute/tests/services/eboActor/fixtures.ts @@ -1,8 +1,8 @@ import { Caip2ChainId, UnixTimestamp } from "@ebo-agent/shared"; import { Address, Hex, pad } from "viem"; +import { ProphetCodec } from "../../../src/index.js"; import { ProtocolContractsAddresses } from "../../../src/interfaces/index.js"; -import { ProphetCodec } from "../../../src/services/prophetCodec.js"; import { Dispute, DisputeId, @@ -23,6 +23,7 @@ export const DEFAULT_MOCKED_PROTOCOL_CONTRACTS: ProtocolContractsAddresses = { eboRequestCreator: "0x9999990000000000000000000000000000000000" as Address, bondEscalationModule: "0x1a2b3c0000000000000000000000000000000000" as Address, horizonAccountingExtension: "0x9999990000000000000000000000000000000000" as Address, + horizonStaking: "0x9999990000000000000000000000000000000000" as Address, }; export const DEFAULT_MOCKED_RESPONSE_DATA: Response = { @@ -101,6 +102,7 @@ export const DEFAULT_MOCKED_REQUEST_CREATED_DATA: Request = { ), finalityModuleData: "0x1311111111111111111111111111111111111111" as Hex, resolutionModuleData: "0x1511111111111111111111111111111111111111" as Hex, + accessModule: "0x1611111111111111111111111111111111111111" as Hex, }, }; diff --git a/packages/automated-dispute/tests/services/eboProcessor.spec.ts b/packages/automated-dispute/tests/services/eboProcessor.spec.ts index 98c88018..a94d5075 100644 --- a/packages/automated-dispute/tests/services/eboProcessor.spec.ts +++ b/packages/automated-dispute/tests/services/eboProcessor.spec.ts @@ -1,4 +1,6 @@ +import { BlockNumberService } from "@ebo-agent/blocknumber"; import { Caip2ChainId, NotificationService, UnixTimestamp } from "@ebo-agent/shared"; +// NOTE: Must import Caip2Utils from here to ensure mock applies correctly import { Caip2Utils } from "@ebo-agent/shared/src/index.js"; import { Block, Hex } from "viem"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; @@ -8,7 +10,7 @@ import { PendingModulesApproval, ProcessorAlreadyStarted, } from "../../src/exceptions/index.js"; -import { ProphetCodec } from "../../src/index.js"; +import { EboActorsManager, EboProcessor, ProphetCodec, ProtocolProvider } from "../../src/index.js"; import { AccountingModules, EboEvent, @@ -28,47 +30,53 @@ const allModulesApproved = Object.values(accountingModules); describe("EboProcessor", () => { let notifier: NotificationService; + let processor: EboProcessor; + let protocolProvider: ProtocolProvider; + let actorsManager: EboActorsManager; + let blockNumberService: BlockNumberService; + + beforeEach(() => { + vi.useFakeTimers(); + notifier = { + send: vi.fn().mockResolvedValue(undefined), + sendOrThrow: vi.fn().mockResolvedValue(undefined), + sendError: vi.fn().mockResolvedValue(undefined), + createErrorMessage: vi.fn((defaultMessage, context) => { + return { + title: defaultMessage, + description: JSON.stringify(context, null, 2), + }; + }), + }; + + const built = mocks.buildEboProcessor(logger, accountingModules, notifier); + processor = built.processor; + protocolProvider = built.protocolProvider; + actorsManager = built.actorsManager; + blockNumberService = built.blockNumberService; + + vi.spyOn(protocolProvider, "isAuthorized").mockResolvedValue(true); + + vi.spyOn(protocolProvider, "getServiceProviderAddress").mockReturnValue( + "0x0000000000000000000000000000000000000001", + ); + vi.spyOn(protocolProvider, "getAccountingModuleAddress").mockReturnValue( + "0x0000000000000000000000000000000000000002", + ); + vi.spyOn(protocolProvider, "getAccountAddress").mockReturnValue( + "0x0000000000000000000000000000000000000003", + ); + }); - describe("start", () => { - const request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; - - beforeEach(() => { - vi.useFakeTimers(); - notifier = { - send: vi.fn().mockResolvedValue(undefined), - sendOrThrow: vi.fn().mockResolvedValue(undefined), - sendError: vi.fn().mockResolvedValue(undefined), - createErrorMessage: vi.fn((defaultMessage, context) => { - return { - title: defaultMessage, - description: JSON.stringify(context, null, 2), - }; - }), - }; - }); - - afterEach(() => { - vi.useRealTimers(); - vi.resetAllMocks(); - }); - - it("throws if at least one module is pending approval", async () => { - const { processor, protocolProvider } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); - - vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue([]); - - const result = processor.start(); - - await expect(result).rejects.toThrow(PendingModulesApproval); - }); + afterEach(() => { + vi.useRealTimers(); + vi.clearAllTimers(); + vi.resetAllMocks(); + }); + describe("start", () => { it("bootstraps actors with onchain active requests when starting", async () => { - const { processor, actorsManager, protocolProvider, blockNumberService } = - mocks.buildEboProcessor(logger, accountingModules, notifier); + const { actor } = mocks.buildEboActor(DEFAULT_MOCKED_REQUEST_CREATED_DATA, logger); const currentEpoch: Epoch = { number: 1n, @@ -81,7 +89,7 @@ describe("EboProcessor", () => { } as unknown as Block; const encodedRequestModuleData = ProphetCodec.encodeRequestRequestModuleData( - request.decodedData.requestModuleData, + DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData, ); const requestCreatedEvent: EboEvent<"RequestCreated"> = { @@ -89,11 +97,11 @@ describe("EboProcessor", () => { blockNumber: 1n, logIndex: 1, timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - requestId: request.id, + requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, metadata: { - requestId: request.id, + requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, request: { - ...request.prophetData, + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, requestModuleData: encodedRequestModuleData, }, ipfsHash: "0x01" as Hex, @@ -106,12 +114,8 @@ describe("EboProcessor", () => { lastFinalizedBlock, ); vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([requestCreatedEvent]); - vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); - vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue(["eip155:1"]); - const { actor } = mocks.buildEboActor(request, logger); - const mockCreateActor = vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); const mockCreateRequest = vi .spyOn(protocolProvider, "createRequest") @@ -119,11 +123,12 @@ describe("EboProcessor", () => { await processor.start(msBetweenChecks); - const { chainId, epoch } = request.decodedData.requestModuleData; + const { chainId, epoch } = + DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData; - expect(mockCreateActor).toBeCalledWith( + expect(mockCreateActor).toHaveBeenCalledWith( expect.objectContaining({ - id: request.id, + id: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, chainId: chainId, epoch: epoch, }), @@ -136,12 +141,6 @@ describe("EboProcessor", () => { }); it("does not create actors to handle unsupported chains", async () => { - const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); - const supportedChains: Caip2ChainId[] = ["eip155:1", "eip155:42161"]; const unsupportedChains: Caip2ChainId[] = ["eip155:9999"]; @@ -238,12 +237,6 @@ describe("EboProcessor", () => { }); it("throws if called more than once", async () => { - const { processor, protocolProvider } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); - const currentEpoch: Epoch = { number: 1n, firstBlockNumber: 1n, @@ -251,7 +244,7 @@ describe("EboProcessor", () => { }; const lastFinalizedBlock = { - number: currentEpoch.firstBlockNumber + 10n, + number: 1n, } as unknown as Block; vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); @@ -285,25 +278,20 @@ describe("EboProcessor", () => { }); it("fetches events since epoch start when starting", async () => { - const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); - const { actor } = mocks.buildEboActor(request, logger); + const { actor } = mocks.buildEboActor(DEFAULT_MOCKED_REQUEST_CREATED_DATA, logger); - const currentEpoch = { + const currentEpoch: Epoch = { number: 1n, firstBlockNumber: 1n, startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, }; - const currentBlock = { + const currentBlock: Block = { number: currentEpoch.firstBlockNumber + 10n, } as unknown as Block; const encodedRequestModuleData = ProphetCodec.encodeRequestRequestModuleData( - request.decodedData.requestModuleData, + DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData, ); const requestCreatedEvent: EboEvent<"RequestCreated"> = { @@ -311,11 +299,11 @@ describe("EboProcessor", () => { blockNumber: 1n, logIndex: 1, timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - requestId: request.id, + requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, metadata: { - requestId: request.id, + requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, request: { - ...request.prophetData, + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, requestModuleData: encodedRequestModuleData, }, ipfsHash: "0x01" as Hex, @@ -343,88 +331,10 @@ describe("EboProcessor", () => { ); vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); - vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); - - const mockGetEvents = vi.spyOn(protocolProvider, "getEvents"); - mockGetEvents.mockResolvedValue([requestCreatedEvent]); - - await processor.start(msBetweenChecks); - - await vi.advanceTimersByTimeAsync(msBetweenChecks); - - expect(mockGetEvents).toHaveBeenCalledWith( - currentEpoch.firstBlockNumber, - currentBlock.number, - ); - }); - - it("drops past events and keeps operating", async () => { - const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); - const { actor } = mocks.buildEboActor(request, logger); - - const currentEpoch = { - number: 1n, - firstBlockNumber: 1n, - startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - }; - - const currentBlock = { - number: currentEpoch.firstBlockNumber + 10n, - } as unknown as Block; - - const encodedRequestModuleData = ProphetCodec.encodeRequestRequestModuleData( - request.decodedData.requestModuleData, - ); - - const requestCreatedEvent: EboEvent<"RequestCreated"> = { - name: "RequestCreated", - blockNumber: 1n, - logIndex: 1, - timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - requestId: request.id, - metadata: { - requestId: request.id, - request: { - ...request.prophetData, - requestModuleData: encodedRequestModuleData, - }, - ipfsHash: "0x01" as Hex, - }, - }; - - vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); - vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); - vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); - vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); - vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([requestCreatedEvent]); - - vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ - "eip155:1", - "eip155:42161", - ]); - - vi.spyOn(Caip2Utils, "isSupported").mockImplementation((chainId: Caip2ChainId) => { - const supportedChains: Caip2ChainId[] = ["eip155:1", "eip155:42161"]; - return supportedChains.includes(chainId); - }); - - processor["handledChainIdsPerEpoch"].set( - currentEpoch.number, - new Set(["eip155:1", "eip155:42161"]), - ); - vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); vi.spyOn(actorsManager, "getActor").mockReturnValue(actor); vi.spyOn(actor, "processEvents").mockImplementation(() => Promise.resolve()); vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor, "canBeTerminated").mockResolvedValue(false); - vi.spyOn(actor, "enqueue").mockImplementation(() => { - throw new PastEventEnqueueError(requestCreatedEvent, requestCreatedEvent); - }); const mockGetEvents = vi.spyOn(protocolProvider, "getEvents"); mockGetEvents.mockResolvedValue([requestCreatedEvent]); @@ -433,540 +343,500 @@ describe("EboProcessor", () => { await vi.advanceTimersByTimeAsync(msBetweenChecks); - expect(logger.warn).toHaveBeenCalledWith( - expect.stringContaining("Dropping already enqueued event"), + expect(mockGetEvents).toHaveBeenCalledWith( + currentEpoch.firstBlockNumber, + currentBlock.number, ); }); - it("keeps the last block checked unaltered when something fails during sync", async () => { - const initialCurrentBlock = 1n; - - const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); - const { actor } = mocks.buildEboActor(request, logger); - - const currentEpoch = { - number: 1n, - firstBlockNumber: 1n, - startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - }; - - const mockProtocolProviderGetEvents = vi - .spyOn(protocolProvider, "getEvents") - .mockImplementationOnce(() => { - throw new Error(); - }) - .mockResolvedValueOnce([]); - + it("logs error and exits the process if not authorized", async () => { vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); - - vi.spyOn(protocolProvider, "getLastFinalizedBlock") - .mockResolvedValueOnce({ number: initialCurrentBlock + 10n } as unknown as Block< - bigint, - false, - "finalized" - >) - .mockResolvedValueOnce({ number: initialCurrentBlock + 20n } as unknown as Block< - bigint, - false, - "finalized" - >); - - vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); - vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); - vi.spyOn(actor, "processEvents").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor, "canBeTerminated").mockResolvedValue(false); - - await processor.start(msBetweenChecks); - - expect(mockProtocolProviderGetEvents).toHaveBeenNthCalledWith( - 1, - currentEpoch.firstBlockNumber, - initialCurrentBlock + 10n, - ); - - expect(mockProtocolProviderGetEvents).toHaveBeenCalledTimes(1); - expect(logger.error).toHaveBeenCalledWith(expect.stringMatching("Sync failed")); - - await vi.advanceTimersByTimeAsync(msBetweenChecks); - - expect(mockProtocolProviderGetEvents).toHaveBeenNthCalledWith( - 2, - currentEpoch.firstBlockNumber, - initialCurrentBlock + 20n, - ); + vi.spyOn(protocolProvider, "isAuthorized").mockResolvedValue(false); + const mockLoggerError = vi.spyOn(logger, "error").mockImplementation(() => {}); + const mockProcessExit = vi + .spyOn(process, "exit") + .mockImplementation((code?: string | number | null | undefined) => { + throw new Error(`process.exit: ${code}`); + }); + const mockSetInterval = vi.spyOn(global, "setInterval"); + + await expect(processor.start(msBetweenChecks)).rejects.toThrow("process.exit: 1"); + expect(mockLoggerError).toHaveBeenCalledWith( + "Authorization required: Operator 0x0000000000000000000000000000000000000003 is not authorized for service provider 0x0000000000000000000000000000000000000001.", + ); + expect(mockProcessExit).toHaveBeenCalledWith(1); + expect(mockSetInterval).not.toHaveBeenCalled(); }); - it("fetches non-consumed events if event fetching fails", async () => { - const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); - const { actor } = mocks.buildEboActor(request, logger); - - const mockLastCheckedBlock = 5n; - processor["lastCheckedBlock"] = mockLastCheckedBlock; - - const currentEpoch = { - number: 1n, - firstBlockNumber: 1n, - startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - }; - - const currentBlock = { - number: currentEpoch.firstBlockNumber + 10n, - } as unknown as Block; - + it("proceeds normally if authorized", async () => { vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); - vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); - vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); - vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); - vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); - vi.spyOn(actor, "processEvents").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor, "canBeTerminated").mockResolvedValue(false); + vi.spyOn(protocolProvider, "isAuthorized").mockResolvedValue(true); + const mockSync = vi.spyOn(processor as any, "sync").mockResolvedValue(undefined); - const mockGetEvents = vi.spyOn(protocolProvider, "getEvents"); - mockGetEvents - .mockImplementationOnce(() => { - throw new Error("Fetch failed"); - }) - .mockResolvedValueOnce([]); - - vi.spyOn(Caip2Utils, "isSupported").mockImplementation((chainId: Caip2ChainId) => { - const supportedChains: Caip2ChainId[] = ["eip155:1", "eip155:42161"]; - return supportedChains.includes(chainId); + const mockSetInterval = vi.spyOn(global, "setInterval").mockImplementation(() => { + return 1 as unknown as NodeJS.Timeout; }); - processor["handledChainIdsPerEpoch"].set( - currentEpoch.number, - new Set(["eip155:1", "eip155:42161"]), - ); + vi.spyOn(global, "clearInterval").mockImplementation(() => {}); await processor.start(msBetweenChecks); - await vi.advanceTimersByTimeAsync(msBetweenChecks); + if (processor["eventsInterval"]) { + clearInterval(processor["eventsInterval"]); + processor["eventsInterval"] = undefined; + } - expect(mockGetEvents).toHaveBeenCalledTimes(2); - expect(mockGetEvents).toHaveBeenNthCalledWith( - 1, - mockLastCheckedBlock + 1n, - currentBlock.number, - ); - expect(mockGetEvents).toHaveBeenNthCalledWith( - 2, - mockLastCheckedBlock + 1n, - currentBlock.number, + expect(protocolProvider.isAuthorized).toHaveBeenCalledWith( + "0x0000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000003", ); + expect(mockSync).toHaveBeenCalled(); + expect(mockSetInterval).toHaveBeenCalledWith(expect.any(Function), msBetweenChecks); }); + }); - it("enqueues and process every new event into the actor", async () => { - const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); - const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA as Request; - const { actor } = mocks.buildEboActor(actorRequest, logger); - - const currentEpoch = { - number: 1n, - firstBlockNumber: 1n, - startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - }; + it("throws if at least one module is pending approval", async () => { + vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue([]); - const currentBlock = { - number: currentEpoch.firstBlockNumber + 10n, - } as unknown as Block; + const result = processor.start(); - vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); - vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); - vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); - vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); + await expect(result).rejects.toThrow(PendingModulesApproval); + }); - const response = mocks.buildResponse(request); - - const decodedRequestModuleData = request.decodedData.requestModuleData; - const encodedRequestModuleData = - ProphetCodec.encodeRequestRequestModuleData(decodedRequestModuleData); - - const eventStream: EboEvent[] = [ - { - name: "RequestCreated", - blockNumber: 6n, - logIndex: 1, - timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - requestId: request.id, - metadata: { - requestId: request.id, - request: { - ...request.prophetData, - requestModuleData: encodedRequestModuleData, - }, - ipfsHash: "0x01" as Hex, - }, - }, - { - name: "ResponseProposed", - blockNumber: 7n, - logIndex: 1, - timestamp: BigInt(Date.UTC(2024, 1, 2, 0, 0, 0, 0)) as UnixTimestamp, - requestId: request.id, - metadata: { - requestId: request.id, - responseId: response.id, - response: response.prophetData, - }, + it("drops past events and keeps operating", async () => { + const { actor } = mocks.buildEboActor(DEFAULT_MOCKED_REQUEST_CREATED_DATA, logger); + + const currentEpoch: Epoch = { + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + }; + + const currentBlock: Block = { + number: currentEpoch.firstBlockNumber + 10n, + } as unknown as Block; + + const encodedRequestModuleData = ProphetCodec.encodeRequestRequestModuleData( + DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData, + ); + + const requestCreatedEvent: EboEvent<"RequestCreated"> = { + name: "RequestCreated", + blockNumber: 1n, + logIndex: 1, + timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, + metadata: { + requestId: DEFAULT_MOCKED_REQUEST_CREATED_DATA.id, + request: { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, + requestModuleData: encodedRequestModuleData, }, - ]; + ipfsHash: "0x01" as Hex, + }, + }; + + vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); + vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); + vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); + vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([requestCreatedEvent]); + + vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ + "eip155:1", + "eip155:42161", + ]); + + vi.spyOn(Caip2Utils, "isSupported").mockImplementation((chainId: Caip2ChainId) => { + const supportedChains: Caip2ChainId[] = ["eip155:1", "eip155:42161"]; + return supportedChains.includes(chainId); + }); - vi.spyOn(protocolProvider, "getEvents") - .mockResolvedValueOnce(eventStream) - .mockResolvedValueOnce([]); + processor["handledChainIdsPerEpoch"].set( + currentEpoch.number, + new Set(["eip155:1", "eip155:42161"]), + ); + + vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); + vi.spyOn(actorsManager, "getActor").mockReturnValue(actor); + vi.spyOn(actor, "processEvents").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor, "canBeTerminated").mockResolvedValue(false); + vi.spyOn(actor, "enqueue").mockImplementation(() => { + throw new PastEventEnqueueError(requestCreatedEvent, requestCreatedEvent); + }); - vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ - "eip155:1", - "eip155:42161", - ]); + const mockGetEvents = vi.spyOn(protocolProvider, "getEvents"); + mockGetEvents.mockResolvedValue([requestCreatedEvent]); - vi.spyOn(actorsManager, "createActor").mockResolvedValue(actor); - vi.spyOn(actorsManager, "getActor").mockReturnValue(actor); - vi.spyOn(actor, "enqueue").mockImplementation(() => {}); - vi.spyOn(actor, "processEvents").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); + await processor.start(msBetweenChecks); - vi.spyOn(Caip2Utils, "isSupported").mockImplementation((chainId: Caip2ChainId) => { - const supportedChains: Caip2ChainId[] = ["eip155:1", "eip155:42161"]; - return supportedChains.includes(chainId); - }); + await vi.advanceTimersByTimeAsync(msBetweenChecks); - processor["handledChainIdsPerEpoch"].set( - currentEpoch.number, - new Set(["eip155:1", "eip155:42161"]), - ); + expect(logger.warn).toHaveBeenCalledWith( + expect.stringContaining("Dropping already enqueued event"), + ); + }); - await processor.start(msBetweenChecks); + it("fetches non-consumed events if event fetching fails", async () => { + const { actor } = mocks.buildEboActor(DEFAULT_MOCKED_REQUEST_CREATED_DATA, logger); - await vi.advanceTimersByTimeAsync(msBetweenChecks); + const mockLastCheckedBlock = 5n; + processor["lastCheckedBlock"] = mockLastCheckedBlock; - expect(actor.enqueue).toHaveBeenCalledTimes(eventStream.length); - expect(actor.enqueue).toHaveBeenNthCalledWith(1, eventStream[0]); - expect(actor.enqueue).toHaveBeenNthCalledWith(2, eventStream[1]); + const currentEpoch = { + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + }; - expect(actor.processEvents).toHaveBeenCalledTimes(1); - expect(actor.onLastBlockUpdated).toHaveBeenCalledTimes(1); - }); + const currentBlock = { + number: currentEpoch.firstBlockNumber + 10n, + } as unknown as Block; - it("enqueues events into corresponding actors", async () => { - const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); + vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); + vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); + vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); + vi.spyOn(actorsManager, "createActor").mockReturnValue(actor); + vi.spyOn(actor, "processEvents").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor, "canBeTerminated").mockResolvedValue(false); - const currentEpoch = { - number: 1n, - firstBlockNumber: 1n, - startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - }; + const mockGetEvents = vi.spyOn(protocolProvider, "getEvents"); + mockGetEvents.mockRejectedValueOnce(new Error("Fetch failed")).mockResolvedValueOnce([]); - const currentBlock = { - number: currentEpoch.firstBlockNumber + 10n, - } as unknown as Block; + vi.spyOn(Caip2Utils, "isSupported").mockImplementation((chainId: Caip2ChainId) => { + const supportedChains: Caip2ChainId[] = ["eip155:1", "eip155:42161"]; + return supportedChains.includes(chainId); + }); - vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); - vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); - vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); - vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); + processor["handledChainIdsPerEpoch"].set( + currentEpoch.number, + new Set(["eip155:1", "eip155:42161"]), + ); + + const mockSendError = vi.spyOn(notifier, "sendError").mockResolvedValue(undefined); + + await processor.start(msBetweenChecks); + + await vi.advanceTimersByTimeAsync(msBetweenChecks); + + expect(mockGetEvents).toHaveBeenCalledTimes(2); + expect(mockGetEvents).toHaveBeenNthCalledWith( + 1, + mockLastCheckedBlock + 1n, + currentBlock.number, + ); + expect(mockGetEvents).toHaveBeenNthCalledWith( + 2, + mockLastCheckedBlock + 1n, + currentBlock.number, + ); + + expect(mockSendError).toHaveBeenCalledWith( + "Error during synchronization", + {}, + expect.any(Error), + ); + }); - const request1: Request = { - ...DEFAULT_MOCKED_REQUEST_CREATED_DATA, - id: "0x01" as RequestId, - decodedData: { - ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData, - requestModuleData: { - ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData, - chainId: "eip155:1" as Caip2ChainId, - }, + it("enqueues and process every new event into the actor", async () => { + const actorRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA as Request; + mocks.buildEboActor(actorRequest, logger); + + const currentEpoch = { + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + }; + + const currentBlock = { + number: currentEpoch.firstBlockNumber + 10n, + timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + } as unknown as Block; + + vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); + vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(currentBlock); + vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); + + const request1: Request = { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA, + id: "0x01" as RequestId, + decodedData: { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData, + requestModuleData: { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData, + chainId: "eip155:1" as Caip2ChainId, }, - prophetData: { - ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, - requestModuleData: ProphetCodec.encodeRequestRequestModuleData({ - epoch: 1n, - chainId: "eip155:1" as Caip2ChainId, - accountingExtension: "0x0000000000000000000000000000000000000000" as Hex, - paymentAmount: 1000n, - }), + }, + prophetData: { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, + requestModuleData: ProphetCodec.encodeRequestRequestModuleData({ + epoch: 1n, + chainId: "eip155:1" as Caip2ChainId, + accountingExtension: "0x0000000000000000000000000000000000000000" as Hex, + paymentAmount: 1000n, + }), + }, + }; + + const request2: Request = { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA, + id: "0x02" as RequestId, + decodedData: { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData, + requestModuleData: { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData, + chainId: "eip155:1" as Caip2ChainId, }, - }; - const response1 = mocks.buildResponse(request1); + }, + prophetData: { + ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, + requestModuleData: ProphetCodec.encodeRequestRequestModuleData({ + epoch: 1n, + chainId: "eip155:1" as Caip2ChainId, + accountingExtension: "0x0000000000000000000000000000000000000000" as Hex, + paymentAmount: 1000n, + }), + }, + }; + const response = mocks.buildResponse(request1); - const request2: Request = { - ...DEFAULT_MOCKED_REQUEST_CREATED_DATA, - id: "0x02" as RequestId, - decodedData: { - ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData, - requestModuleData: { - ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData, - chainId: "eip155:137" as Caip2ChainId, - }, - }, - prophetData: { - ...DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, - requestModuleData: ProphetCodec.encodeRequestRequestModuleData({ - epoch: 1n, - chainId: "eip155:137" as Caip2ChainId, - accountingExtension: "0x0000000000000000000000000000000000000000" as Hex, - paymentAmount: 1000n, - }), - }, - }; - const response2 = mocks.buildResponse(request2); - - const eventStream: EboEvent[] = [ - { - name: "ResponseProposed", - blockNumber: 7n, - logIndex: 1, - timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + const decodedRequestModuleData = + DEFAULT_MOCKED_REQUEST_CREATED_DATA.decodedData.requestModuleData; + const encodedRequestModuleData = + ProphetCodec.encodeRequestRequestModuleData(decodedRequestModuleData); + + const eventStream: EboEvent[] = [ + { + name: "RequestCreated", + blockNumber: 6n, + logIndex: 1, + timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + requestId: request1.id, + metadata: { requestId: request1.id, - metadata: { - requestId: request1.id, - responseId: response1.id, - response: response1.prophetData, + request: { + ...request1.prophetData, + requestModuleData: encodedRequestModuleData, }, + ipfsHash: "0x01" as Hex, }, - { - name: "ResponseProposed", - blockNumber: 7n, - logIndex: 2, - timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + }, + { + name: "ResponseProposed", + blockNumber: 7n, + logIndex: 1, + timestamp: BigInt(Date.UTC(2024, 1, 2, 0, 0, 0, 0)) as UnixTimestamp, + requestId: request2.id, + metadata: { requestId: request2.id, - metadata: { - requestId: request2.id, - responseId: response2.id, - response: response2.prophetData, - }, + responseId: response.id, + response: response.prophetData, }, - ]; - - vi.spyOn(protocolProvider, "getEvents") - .mockResolvedValueOnce(eventStream) - .mockResolvedValueOnce([]); - - vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ - "eip155:1", - "eip155:42161", - "eip155:137", - ]); - - const actor1 = mocks.buildEboActor(request1, logger).actor; - const actor2 = mocks.buildEboActor(request2, logger).actor; - - vi.spyOn(actorsManager, "createActor") - .mockResolvedValueOnce(actor1) - .mockResolvedValueOnce(actor2); - vi.spyOn(actorsManager, "getActor").mockImplementation((requestId: RequestId) => { - switch (requestId) { - case request1.id: - return actor1; - case request2.id: - return actor2; - default: - return undefined; - } - }); - - vi.spyOn(actor1, "enqueue").mockImplementation(() => {}); - vi.spyOn(actor2, "enqueue").mockImplementation(() => {}); - vi.spyOn(actor1, "processEvents").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor2, "processEvents").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor1, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); - vi.spyOn(actor2, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); - - vi.spyOn(Caip2Utils, "isSupported").mockImplementation((chainId: Caip2ChainId) => { - const supportedChains: Caip2ChainId[] = ["eip155:1", "eip155:42161", "eip155:137"]; - return supportedChains.includes(chainId); - }); - - processor["handledChainIdsPerEpoch"].set( - currentEpoch.number, - new Set(["eip155:1", "eip155:42161", "eip155:137"]), - ); - - await processor.start(msBetweenChecks); - - await vi.advanceTimersByTimeAsync(msBetweenChecks); + }, + ]; + + vi.spyOn(protocolProvider, "getEvents") + .mockResolvedValueOnce(eventStream) + .mockResolvedValueOnce([]); + + vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ + "eip155:1", + "eip155:42161", + "eip155:137", + ]); + + const actor1 = mocks.buildEboActor(request1, logger).actor; + const actor2 = mocks.buildEboActor(request2, logger).actor; + + vi.spyOn(actorsManager, "createActor") + .mockResolvedValueOnce(actor1) + .mockResolvedValueOnce(actor2); + + vi.spyOn(actorsManager, "getActor").mockImplementation((requestId: RequestId) => { + switch (requestId) { + case request1.id: + return actor1; + case request2.id: + return actor2; + default: + return undefined; + } + }); - expect(actor1.enqueue).toHaveBeenCalledWith(eventStream[0]); - expect(actor2.enqueue).toHaveBeenCalledWith(eventStream[1]); + const mockEnqueue1 = vi.spyOn(actor1, "enqueue").mockImplementation(() => {}); + const mockEnqueue2 = vi.spyOn(actor2, "enqueue").mockImplementation(() => {}); + vi.spyOn(actor1, "processEvents").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor2, "processEvents").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor1, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); + vi.spyOn(actor2, "onLastBlockUpdated").mockImplementation(() => Promise.resolve()); - expect(actor1.processEvents).toHaveBeenCalledTimes(1); - expect(actor2.processEvents).toHaveBeenCalledTimes(1); + vi.spyOn(Caip2Utils, "isSupported").mockImplementation((chainId: Caip2ChainId) => { + const supportedChains: Caip2ChainId[] = ["eip155:1", "eip155:42161", "eip155:137"]; + return supportedChains.includes(chainId); }); - it("does not create a new request if a corresponding actor already exist", async () => { - const { processor, protocolProvider, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); + processor["handledChainIdsPerEpoch"].set( + currentEpoch.number, + new Set(["eip155:1", "eip155:42161", "eip155:137"]), + ); - const currentEpoch: Epoch = { - number: 1n, - firstBlockNumber: 1n, - startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - }; + await processor.start(msBetweenChecks); - const lastFinalizedBlock = { - number: 1n, - } as unknown as Block; + await vi.advanceTimersByTimeAsync(msBetweenChecks); + await vi.advanceTimersByTimeAsync(msBetweenChecks); - vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); - vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); - vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue( - lastFinalizedBlock, - ); - vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([]); - vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ - "eip155:1", - "eip155:42161", - ]); + expect(mockEnqueue1).toHaveBeenCalledWith(eventStream[0]); + expect(mockEnqueue2).toHaveBeenCalledWith(eventStream[1]); - vi.spyOn(actorsManager, "getActorsRequests").mockReturnValue([ - { id: "0x01" as RequestId, chainId: "eip155:1", epoch: currentEpoch.number }, - { id: "0x02" as RequestId, chainId: "eip155:42161", epoch: currentEpoch.number }, - ]); + expect(actor1.processEvents).toHaveBeenCalledTimes(1); + expect(actor2.processEvents).toHaveBeenCalledTimes(1); + }); - processor["handledChainIdsPerEpoch"].set( - currentEpoch.number, - new Set(["eip155:1", "eip155:42161"]), - ); + it("does not create a new request if a corresponding actor already exist", async () => { + const currentEpoch: Epoch = { + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + }; - const mockProtocolProviderCreateRequest = vi - .spyOn(protocolProvider, "createRequest") - .mockImplementation(() => Promise.resolve()); + const lastFinalizedBlock = { + number: 1n, + } as unknown as Block; - await processor.start(); + vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); + vi.spyOn(protocolProvider, "getLastFinalizedBlock").mockResolvedValue(lastFinalizedBlock); + vi.spyOn(protocolProvider, "getEvents").mockResolvedValue([]); + vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue([ + "eip155:1", + "eip155:42161", + ]); - expect(mockProtocolProviderCreateRequest).not.toHaveBeenCalled(); - }); + vi.spyOn(actorsManager, "getActorsRequests").mockReturnValue([ + { id: "0x01" as RequestId, chainId: "eip155:1", epoch: currentEpoch.number }, + { id: "0x02" as RequestId, chainId: "eip155:42161", epoch: currentEpoch.number }, + ]); - it("adds handled chain IDs when actors are created", async () => { - const { processor, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); + processor["handledChainIdsPerEpoch"].set(1n, new Set(["eip155:1", "eip155:42161"])); - const requestId = "0x01" as RequestId; - const epoch = 1n; - const chainId = "eip155:1" as Caip2ChainId; + const mockProtocolProviderCreateRequest = vi + .spyOn(protocolProvider, "createRequest") + .mockImplementation(() => Promise.resolve()); - const firstEvent: EboEvent<"RequestCreated"> = { - name: "RequestCreated", - requestId, - blockNumber: 1n, - logIndex: 1, - timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - metadata: { - requestId: "0x01" as RequestId, - request: DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, - ipfsHash: "0x01" as Hex, - }, - }; + await processor.start(msBetweenChecks); - vi.spyOn(actorsManager, "getActor").mockReturnValue(undefined); - vi.spyOn(actorsManager, "createActor").mockImplementation(() => { - const mockRequest: Request = DEFAULT_MOCKED_REQUEST_CREATED_DATA; - return mocks.buildEboActor(mockRequest, logger).actor; - }); + expect(mockProtocolProviderCreateRequest).not.toHaveBeenCalled(); + }); - await processor["getOrCreateActor"](requestId, firstEvent); + it("adds handled chain IDs when actors are created", async () => { + const { processor, actorsManager } = mocks.buildEboProcessor( + logger, + accountingModules, + notifier, + ); + + const requestId = "0x01" as RequestId; + const epoch = 1n; + const chainId = "eip155:1" as Caip2ChainId; + + const firstEvent: EboEvent<"RequestCreated"> = { + name: "RequestCreated", + requestId, + blockNumber: 1n, + logIndex: 1, + timestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + metadata: { + requestId: "0x01" as RequestId, + request: DEFAULT_MOCKED_REQUEST_CREATED_DATA.prophetData, + ipfsHash: "0x01" as Hex, + }, + }; + + vi.spyOn(actorsManager, "getActor").mockReturnValue(undefined); + vi.spyOn(actorsManager, "createActor").mockImplementation(() => { + return mocks.buildEboActor(DEFAULT_MOCKED_REQUEST_CREATED_DATA, logger).actor; + }); - const handledChainIds = processor["getHandledChainIds"](epoch); + await processor["getOrCreateActor"](requestId, firstEvent); - expect(handledChainIds).toBeDefined(); - expect(handledChainIds!.has(chainId)).toBe(true); - }); + const handledChainIds = processor["getHandledChainIds"](epoch); - it("retains handled chain IDs after actors are terminated", async () => { - const { processor, actorsManager } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); + expect(handledChainIds).toBeDefined(); + expect(handledChainIds!.has(chainId)).toBe(true); + }); - const requestId = "0x01" as RequestId; - const epoch = 1n; - const chainId = "eip155:1" as Caip2ChainId; + it("retains handled chain IDs after actors are terminated", async () => { + const { processor, actorsManager } = mocks.buildEboProcessor( + logger, + accountingModules, + notifier, + ); - const mockRequest = DEFAULT_MOCKED_REQUEST_CREATED_DATA; + const requestId = "0x01" as RequestId; + const epoch = 1n; + const chainId = "eip155:1" as Caip2ChainId; - const actor = mocks.buildEboActor(mockRequest, logger).actor; + const actor = mocks.buildEboActor(DEFAULT_MOCKED_REQUEST_CREATED_DATA, logger).actor; - vi.spyOn(actorsManager, "getActor").mockReturnValue(actor); - vi.spyOn(actorsManager, "deleteActor").mockReturnValue(true); + vi.spyOn(actorsManager, "getActor").mockReturnValue(actor); + vi.spyOn(actorsManager, "deleteActor").mockReturnValue(true); - processor["addHandledChainId"](epoch, chainId); + processor["addHandledChainId"](epoch, chainId); - await processor["terminateActor"](requestId); + await processor["terminateActor"](requestId); - const handledChainIds = processor["getHandledChainIds"](epoch); + const handledChainIds = processor["getHandledChainIds"](epoch); - expect(handledChainIds).toBeDefined(); - expect(handledChainIds!.has(chainId)).toBe(true); - }); + expect(handledChainIds).toBeDefined(); + expect(handledChainIds!.has(chainId)).toBe(true); + }); - it("cleans up epochs less than currentEpoch - 1", () => { - const { processor } = mocks.buildEboProcessor(logger, accountingModules, notifier); + it("cleans up epochs less than currentEpoch - 1", () => { + const { processor } = mocks.buildEboProcessor(logger, accountingModules, notifier); - processor["handledChainIdsPerEpoch"].set(1n, new Set(["eip155:1"])); - processor["handledChainIdsPerEpoch"].set(2n, new Set(["eip155:1"])); - processor["handledChainIdsPerEpoch"].set(3n, new Set(["eip155:1"])); + processor["handledChainIdsPerEpoch"].set(1n, new Set(["eip155:1"])); + processor["handledChainIdsPerEpoch"].set(2n, new Set(["eip155:1"])); + processor["handledChainIdsPerEpoch"].set(3n, new Set(["eip155:1"])); - processor["cleanupOldEpochs"](3n); + processor["cleanupOldEpochs"](3n); - expect(processor["handledChainIdsPerEpoch"].has(1n)).toBe(false); - expect(processor["handledChainIdsPerEpoch"].has(2n)).toBe(true); - expect(processor["handledChainIdsPerEpoch"].has(3n)).toBe(true); - }); + expect(processor["handledChainIdsPerEpoch"].has(1n)).toBe(false); + expect(processor["handledChainIdsPerEpoch"].has(2n)).toBe(true); + expect(processor["handledChainIdsPerEpoch"].has(3n)).toBe(true); + }); - it("does not create requests for already handled chain IDs", async () => { - const { processor, protocolProvider } = mocks.buildEboProcessor( - logger, - accountingModules, - notifier, - ); + it("does not create requests for already handled chain IDs", async () => { + const { processor, protocolProvider } = mocks.buildEboProcessor( + logger, + accountingModules, + notifier, + ); - const currentEpoch = { - number: 1n, - firstBlockNumber: 1n, - startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, - }; + const currentEpoch = { + number: 1n, + firstBlockNumber: 1n, + startTimestamp: BigInt(Date.UTC(2024, 1, 1, 0, 0, 0, 0)) as UnixTimestamp, + }; - const availableChains: Caip2ChainId[] = ["eip155:1", "eip155:42161"]; + const availableChains: Caip2ChainId[] = ["eip155:1", "eip155:42161"]; - vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); - vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); - vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); - vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue(availableChains); + vi.spyOn(protocolProvider, "getApprovedModules").mockResolvedValue(allModulesApproved); + vi.spyOn(protocolProvider, "getCurrentEpoch").mockResolvedValue(currentEpoch); + vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); + vi.spyOn(protocolProvider, "getAvailableChains").mockResolvedValue(availableChains); - processor["handledChainIdsPerEpoch"].set(1n, new Set(["eip155:1"])); + processor["handledChainIdsPerEpoch"].set(1n, new Set(["eip155:1"])); - const createRequestSpy = vi - .spyOn(protocolProvider, "createRequest") - .mockResolvedValue(); + const createRequestSpy = vi.spyOn(protocolProvider, "createRequest").mockResolvedValue(); - await processor["createMissingRequests"](currentEpoch.number); + await processor["createMissingRequests"](currentEpoch.number); - expect(createRequestSpy).toHaveBeenCalledTimes(1); - expect(createRequestSpy).toHaveBeenCalledWith(currentEpoch.number, "eip155:42161"); - }); + expect(createRequestSpy).toHaveBeenCalledTimes(1); + expect(createRequestSpy).toHaveBeenCalledWith(currentEpoch.number, "eip155:42161"); }); }); diff --git a/packages/automated-dispute/tests/services/protocolProvider.spec.ts b/packages/automated-dispute/tests/services/protocolProvider.spec.ts index b3e4f0d9..c5412e83 100644 --- a/packages/automated-dispute/tests/services/protocolProvider.spec.ts +++ b/packages/automated-dispute/tests/services/protocolProvider.spec.ts @@ -20,6 +20,7 @@ import { eboRequestCreatorAbi, epochManagerAbi, horizonAccountingExtensionAbi, + horizonStakingAbi, oracleAbi, } from "../../src/abis/index.js"; import { @@ -96,6 +97,7 @@ describe("ProtocolProvider", () => { eboRequestCreator: "0x1234567890123456789012345678901234567890", bondEscalationModule: "0x1234567890123456789012345678901234567890", horizonAccountingExtension: "0x1234567890123456789012345678901234567890", + horizonStaking: "0x1234567890123456789012345678901234567890", }; const mockBlockNumberService = { @@ -160,6 +162,14 @@ describe("ProtocolProvider", () => { }, }; } + if (abi === horizonStakingAbi && address === mockContractAddress.horizonStaking) { + return { + address, + read: { + isAuthorized: vi.fn(), + }, + }; + } throw new Error("Invalid contract address or ABI"); }); @@ -1333,4 +1343,66 @@ describe("ProtocolProvider", () => { expect(protocolProvider.getServiceProviderAddress()).toBe(mockDerivedAddress); }); }); + + describe("isAuthorized", () => { + it("returns true when operator is authorized", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfigBase, + mockContractAddress, + mockedPrivateKey, + mockServiceProviderAddress, + mockLogger(), + mockBlockNumberService, + ); + + ( + protocolProvider["horizonStakingContract"].read.isAuthorized as Mock + ).mockResolvedValue(true); + + const serviceProvider = "0xServiceProvider" as Address; + const verifier = "0xVerifier" as Address; + const operator = "0xOperator" as Address; + + const result = await protocolProvider.read.isAuthorized( + serviceProvider, + verifier, + operator, + ); + + expect(result).toBe(true); + expect( + protocolProvider["horizonStakingContract"].read.isAuthorized, + ).toHaveBeenCalledWith([serviceProvider, verifier, operator]); + }); + + it("returns false when operator is not authorized", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfigBase, + mockContractAddress, + mockedPrivateKey, + mockServiceProviderAddress, + mockLogger(), + mockBlockNumberService, + ); + + ( + protocolProvider["horizonStakingContract"].read.isAuthorized as Mock + ).mockResolvedValue(false); + + const serviceProvider = "0xServiceProvider" as Address; + const verifier = "0xVerifier" as Address; + const operator = "0xOperator" as Address; + + const result = await protocolProvider.read.isAuthorized( + serviceProvider, + verifier, + operator, + ); + + expect(result).toBe(false); + expect( + protocolProvider["horizonStakingContract"].read.isAuthorized, + ).toHaveBeenCalledWith([serviceProvider, verifier, operator]); + }); + }); });