diff --git a/apps/agent/config.example.yml b/apps/agent/config.example.yml index 2813274..3a09782 100644 --- a/apps/agent/config.example.yml +++ b/apps/agent/config.example.yml @@ -8,6 +8,7 @@ protocolProvider: epochManager: "0x1234567890123456789012345678901234567890" eboRequestCreator: "0x1234567890123456789012345678901234567890" bondEscalationModule: "0x1234567890123456789012345678901234567890" + horizonAccountingExtension: "0x1234567890123456789012345678901234567890" blockNumberService: blockmetaConfig: diff --git a/apps/agent/src/config/schemas.ts b/apps/agent/src/config/schemas.ts index d5b387a..89089bf 100644 --- a/apps/agent/src/config/schemas.ts +++ b/apps/agent/src/config/schemas.ts @@ -43,6 +43,7 @@ const protocolProviderConfigSchema = z.object({ epochManager: addressSchema, eboRequestCreator: addressSchema, bondEscalationModule: addressSchema, + horizonAccountingExtension: addressSchema, }), }); diff --git a/packages/automated-dispute/src/abis/horizonAccountingExtension.ts b/packages/automated-dispute/src/abis/horizonAccountingExtension.ts new file mode 100644 index 0000000..87aa601 --- /dev/null +++ b/packages/automated-dispute/src/abis/horizonAccountingExtension.ts @@ -0,0 +1,430 @@ +export const horizonAccountingExtensionAbi = [ + { + type: "constructor", + inputs: [ + { name: "_horizonStaking", type: "address", internalType: "contract IHorizonStaking" }, + { name: "_oracle", type: "address", internalType: "contract IOracle" }, + { name: "_grt", type: "address", internalType: "contract IERC20" }, + { name: "_minThawingPeriod", type: "uint256", internalType: "uint256" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "GRT", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "contract IERC20" }], + stateMutability: "view", + }, + { + type: "function", + name: "HORIZON_STAKING", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "contract IHorizonStaking" }], + stateMutability: "view", + }, + { + type: "function", + name: "MAX_SLASHING_USERS", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "MAX_USERS_TO_CHECK", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "MAX_VERIFIER_CUT", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "MIN_THAWING_PERIOD", + inputs: [], + outputs: [{ name: "", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "ORACLE", + inputs: [], + outputs: [{ name: "", type: "address", internalType: "contract IOracle" }], + stateMutability: "view", + }, + { + type: "function", + name: "approveModule", + inputs: [{ name: "_module", type: "address", internalType: "address" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "approvedModules", + inputs: [{ name: "_user", type: "address", internalType: "address" }], + outputs: [{ name: "_approvedModules", type: "address[]", internalType: "address[]" }], + stateMutability: "view", + }, + { + type: "function", + name: "bond", + inputs: [ + { name: "_bonder", type: "address", internalType: "address" }, + { name: "_requestId", type: "bytes32", internalType: "bytes32" }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + { name: "_sender", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "bond", + inputs: [ + { name: "_bonder", type: "address", internalType: "address" }, + { name: "_requestId", type: "bytes32", internalType: "bytes32" }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "bondedForRequest", + inputs: [ + { name: "_bonder", type: "address", internalType: "address" }, + { name: "_requestId", type: "bytes32", internalType: "bytes32" }, + ], + outputs: [{ name: "_amount", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "claimEscalationReward", + inputs: [ + { name: "_disputeId", type: "bytes32", internalType: "bytes32" }, + { name: "_pledger", type: "address", internalType: "address" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "escalationResults", + inputs: [{ name: "_disputeId", type: "bytes32", internalType: "bytes32" }], + outputs: [ + { name: "requestId", type: "bytes32", internalType: "bytes32" }, + { name: "amountPerPledger", type: "uint256", internalType: "uint256" }, + { name: "bondSize", type: "uint256", internalType: "uint256" }, + { + name: "bondEscalationModule", + type: "address", + internalType: "contract IBondEscalationModule", + }, + ], + stateMutability: "view", + }, + { + type: "function", + name: "onSettleBondEscalation", + inputs: [ + { + name: "_request", + type: "tuple", + internalType: "struct IOracle.Request", + components: [ + { name: "nonce", type: "uint96", internalType: "uint96" }, + { name: "requester", type: "address", internalType: "address" }, + { name: "requestModule", type: "address", internalType: "address" }, + { name: "responseModule", type: "address", internalType: "address" }, + { name: "disputeModule", type: "address", internalType: "address" }, + { name: "resolutionModule", type: "address", internalType: "address" }, + { name: "finalityModule", type: "address", internalType: "address" }, + { name: "requestModuleData", type: "bytes", internalType: "bytes" }, + { name: "responseModuleData", type: "bytes", internalType: "bytes" }, + { name: "disputeModuleData", type: "bytes", internalType: "bytes" }, + { name: "resolutionModuleData", type: "bytes", internalType: "bytes" }, + { name: "finalityModuleData", type: "bytes", internalType: "bytes" }, + ], + }, + { + name: "_dispute", + type: "tuple", + internalType: "struct IOracle.Dispute", + components: [ + { name: "disputer", type: "address", internalType: "address" }, + { name: "proposer", type: "address", internalType: "address" }, + { name: "responseId", type: "bytes32", internalType: "bytes32" }, + { name: "requestId", type: "bytes32", internalType: "bytes32" }, + ], + }, + { name: "_amountPerPledger", type: "uint256", internalType: "uint256" }, + { name: "_winningPledgersLength", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "pay", + inputs: [ + { name: "_requestId", type: "bytes32", internalType: "bytes32" }, + { name: "_payer", type: "address", internalType: "address" }, + { name: "_receiver", type: "address", internalType: "address" }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "pledge", + inputs: [ + { name: "_pledger", type: "address", internalType: "address" }, + { + name: "_request", + type: "tuple", + internalType: "struct IOracle.Request", + components: [ + { name: "nonce", type: "uint96", internalType: "uint96" }, + { name: "requester", type: "address", internalType: "address" }, + { name: "requestModule", type: "address", internalType: "address" }, + { name: "responseModule", type: "address", internalType: "address" }, + { name: "disputeModule", type: "address", internalType: "address" }, + { name: "resolutionModule", type: "address", internalType: "address" }, + { name: "finalityModule", type: "address", internalType: "address" }, + { name: "requestModuleData", type: "bytes", internalType: "bytes" }, + { name: "responseModuleData", type: "bytes", internalType: "bytes" }, + { name: "disputeModuleData", type: "bytes", internalType: "bytes" }, + { name: "resolutionModuleData", type: "bytes", internalType: "bytes" }, + { name: "finalityModuleData", type: "bytes", internalType: "bytes" }, + ], + }, + { + name: "_dispute", + type: "tuple", + internalType: "struct IOracle.Dispute", + components: [ + { name: "disputer", type: "address", internalType: "address" }, + { name: "proposer", type: "address", internalType: "address" }, + { name: "responseId", type: "bytes32", internalType: "bytes32" }, + { name: "requestId", type: "bytes32", internalType: "bytes32" }, + ], + }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "pledgerClaimed", + inputs: [ + { name: "_requestId", type: "bytes32", internalType: "bytes32" }, + { name: "_pledger", type: "address", internalType: "address" }, + ], + outputs: [{ name: "_claimed", type: "bool", internalType: "bool" }], + stateMutability: "view", + }, + { + type: "function", + name: "pledges", + inputs: [{ name: "_disputeId", type: "bytes32", internalType: "bytes32" }], + outputs: [{ name: "_amount", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + name: "release", + inputs: [ + { name: "_bonder", type: "address", internalType: "address" }, + { name: "_requestId", type: "bytes32", internalType: "bytes32" }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "releasePledge", + inputs: [ + { + name: "_request", + type: "tuple", + internalType: "struct IOracle.Request", + components: [ + { name: "nonce", type: "uint96", internalType: "uint96" }, + { name: "requester", type: "address", internalType: "address" }, + { name: "requestModule", type: "address", internalType: "address" }, + { name: "responseModule", type: "address", internalType: "address" }, + { name: "disputeModule", type: "address", internalType: "address" }, + { name: "resolutionModule", type: "address", internalType: "address" }, + { name: "finalityModule", type: "address", internalType: "address" }, + { name: "requestModuleData", type: "bytes", internalType: "bytes" }, + { name: "responseModuleData", type: "bytes", internalType: "bytes" }, + { name: "disputeModuleData", type: "bytes", internalType: "bytes" }, + { name: "resolutionModuleData", type: "bytes", internalType: "bytes" }, + { name: "finalityModuleData", type: "bytes", internalType: "bytes" }, + ], + }, + { + name: "_dispute", + type: "tuple", + internalType: "struct IOracle.Dispute", + components: [ + { name: "disputer", type: "address", internalType: "address" }, + { name: "proposer", type: "address", internalType: "address" }, + { name: "responseId", type: "bytes32", internalType: "bytes32" }, + { name: "requestId", type: "bytes32", internalType: "bytes32" }, + ], + }, + { name: "_pledger", type: "address", internalType: "address" }, + { name: "_amount", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "revokeModule", + inputs: [{ name: "_module", type: "address", internalType: "address" }], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "slash", + inputs: [ + { name: "_disputeId", type: "bytes32", internalType: "bytes32" }, + { name: "_usersToSlash", type: "uint256", internalType: "uint256" }, + { name: "_maxUsersToCheck", type: "uint256", internalType: "uint256" }, + ], + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + name: "totalBonded", + inputs: [{ name: "_user", type: "address", internalType: "address" }], + outputs: [{ name: "_bonded", type: "uint256", internalType: "uint256" }], + stateMutability: "view", + }, + { + type: "event", + name: "BondEscalationSettled", + inputs: [ + { name: "_requestId", type: "bytes32", indexed: false, internalType: "bytes32" }, + { name: "_disputeId", type: "bytes32", indexed: false, internalType: "bytes32" }, + { name: "_amountPerPledger", type: "uint256", indexed: false, internalType: "uint256" }, + { + name: "_winningPledgersLength", + type: "uint256", + indexed: false, + internalType: "uint256", + }, + ], + anonymous: false, + }, + { + type: "event", + name: "Bonded", + inputs: [ + { name: "_requestId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_bonder", type: "address", indexed: true, internalType: "address" }, + { name: "_amount", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "EscalationRewardClaimed", + inputs: [ + { name: "_requestId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_disputeId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_pledger", type: "address", indexed: true, internalType: "address" }, + { name: "_reward", type: "uint256", indexed: false, internalType: "uint256" }, + { name: "_released", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "Paid", + inputs: [ + { name: "_requestId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_beneficiary", type: "address", indexed: true, internalType: "address" }, + { name: "_payer", type: "address", indexed: true, internalType: "address" }, + { name: "_amount", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "PledgeReleased", + inputs: [ + { name: "_requestId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_disputeId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_pledger", type: "address", indexed: true, internalType: "address" }, + { name: "_amount", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "Pledged", + inputs: [ + { name: "_pledger", type: "address", indexed: true, internalType: "address" }, + { name: "_requestId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_disputeId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_amount", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "Released", + inputs: [ + { name: "_requestId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_beneficiary", type: "address", indexed: true, internalType: "address" }, + { name: "_amount", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { + type: "event", + name: "WinningPledgersPaid", + inputs: [ + { name: "_requestId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { name: "_disputeId", type: "bytes32", indexed: true, internalType: "bytes32" }, + { + name: "_winningPledgers", + type: "address[]", + indexed: true, + internalType: "address[]", + }, + { name: "_amountPerPledger", type: "uint256", indexed: false, internalType: "uint256" }, + ], + anonymous: false, + }, + { type: "error", name: "HorizonAccountingExtension_AlreadyClaimed", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_AlreadySettled", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_FeeOnTransferToken", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_InsufficientBondedTokens", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_InsufficientFunds", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_InsufficientTokens", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_InvalidMaxVerifierCut", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_InvalidThawingPeriod", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_NoEscalationResult", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_NotAllowed", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_UnauthorizedModule", inputs: [] }, + { type: "error", name: "HorizonAccountingExtension_UnauthorizedUser", inputs: [] }, + { type: "error", name: "Validator_InvalidDispute", inputs: [] }, + { type: "error", name: "Validator_InvalidResponse", inputs: [] }, +] as const; diff --git a/packages/automated-dispute/src/abis/index.ts b/packages/automated-dispute/src/abis/index.ts index b958884..f5c80b6 100644 --- a/packages/automated-dispute/src/abis/index.ts +++ b/packages/automated-dispute/src/abis/index.ts @@ -2,3 +2,4 @@ export * from "./oracle.js"; export * from "./epochManager.js"; export * from "./eboRequestCreator.js"; export * from "./bondEscalationModule.js"; +export * from "./horizonAccountingExtension.js"; diff --git a/packages/automated-dispute/src/constants.ts b/packages/automated-dispute/src/constants.ts index f976123..e2290c5 100644 --- a/packages/automated-dispute/src/constants.ts +++ b/packages/automated-dispute/src/constants.ts @@ -3,4 +3,5 @@ export const ProtocolContractsNames = [ "epochManager", "eboRequestCreator", "bondEscalationModule", + "horizonAccountingExtension", ] as const; diff --git a/packages/automated-dispute/src/interfaces/protocolProvider.ts b/packages/automated-dispute/src/interfaces/protocolProvider.ts index 5508e18..f2ab854 100644 --- a/packages/automated-dispute/src/interfaces/protocolProvider.ts +++ b/packages/automated-dispute/src/interfaces/protocolProvider.ts @@ -53,6 +53,14 @@ export interface IReadProvider { * @returns A promise that resolves with an array of approved modules. */ getAccountingApprovedModules(): Promise; + + /** + * Gets the list of approved modules' addresses for a given wallet address. + * + * @param user The address of the user. + * @returns A promise that resolves with an array of approved modules for the user. + */ + getApprovedModules(user: Address): Promise; } /** @@ -161,6 +169,14 @@ export interface IWriteProvider { * @param modules an array of addresses for the modules to be approved */ approveAccountingModules(modules: Address[]): Promise; + + /** + * Approves a module in the accounting extension contract. + * + * @param module The address of the module to approve. + * @returns A promise that resolves when the module is approved. + */ + approveModule(module: Address): Promise; } /** diff --git a/packages/automated-dispute/src/providers/protocolProvider.ts b/packages/automated-dispute/src/providers/protocolProvider.ts index 842eaa0..8fffc58 100644 --- a/packages/automated-dispute/src/providers/protocolProvider.ts +++ b/packages/automated-dispute/src/providers/protocolProvider.ts @@ -35,6 +35,7 @@ import { bondEscalationModuleAbi, eboRequestCreatorAbi, epochManagerAbi, + horizonAccountingExtensionAbi, oracleAbi, } from "../abis/index.js"; import { @@ -104,6 +105,12 @@ export class ProtocolProvider implements IProtocolProvider { Address >; + private horizonAccountingExtensionContract: GetContractReturnType< + typeof horizonAccountingExtensionAbi, + typeof this.writeClient, + Address + >; + /** * Creates a new ProtocolProvider instance * @param rpcUrls The RPC URLs to connect to the Arbitrum chain @@ -175,6 +182,14 @@ export class ProtocolProvider implements IProtocolProvider { wallet: this.writeClient, }, }); + this.horizonAccountingExtensionContract = getContract({ + address: contracts.horizonAccountingExtension, + abi: horizonAccountingExtensionAbi, + client: { + public: this.readClient, + wallet: this.writeClient, + }, + }); } public write: IWriteProvider = { @@ -187,6 +202,7 @@ export class ProtocolProvider implements IProtocolProvider { escalateDispute: this.escalateDispute.bind(this), finalize: this.finalize.bind(this), approveAccountingModules: this.approveAccountingModules.bind(this), + approveModule: this.approveModule.bind(this), }; public read: IReadProvider = { @@ -196,6 +212,7 @@ export class ProtocolProvider implements IProtocolProvider { getAvailableChains: this.getAvailableChains.bind(this), getAccountingModuleAddress: this.getAccountingModuleAddress.bind(this), getAccountingApprovedModules: this.getAccountingApprovedModules.bind(this), + getApprovedModules: this.getApprovedModules.bind(this), }; /** @@ -301,6 +318,44 @@ export class ProtocolProvider implements IProtocolProvider { return "0x01"; } + /** + * Approves a module in the accounting extension contract. + * + * @param module The address of the module to approve. + * @throws {TransactionExecutionError} Throws if the transaction fails during execution. + * @returns {Promise} A promise that resolves when the module is approved. + */ + async approveModule(module: Address): Promise { + const { request: simulatedRequest } = await this.readClient.simulateContract({ + address: this.horizonAccountingExtensionContract.address, + abi: horizonAccountingExtensionAbi, + functionName: "approveModule", + args: [module], + account: this.writeClient.account, + }); + + const hash = await this.writeClient.writeContract(simulatedRequest); + + const receipt = await this.readClient.waitForTransactionReceipt({ + hash, + confirmations: this.rpcConfig.transactionReceiptConfirmations, + }); + + if (receipt.status !== "success") { + throw new TransactionExecutionError("approveModule transaction failed"); + } + } + + /** + * Gets the list of approved modules' addresses for a given user. + * + * @param user The address of the user. + * @returns A promise that resolves with an array of approved modules for the user. + */ + async getApprovedModules(user: Address): Promise { + return [...(await this.horizonAccountingExtensionContract.read.approvedModules([user]))]; + } + async getAccountingApprovedModules(): Promise { // TODO: implement actual method return []; diff --git a/packages/automated-dispute/tests/services/protocolProvider.spec.ts b/packages/automated-dispute/tests/services/protocolProvider.spec.ts index a388bcd..215f694 100644 --- a/packages/automated-dispute/tests/services/protocolProvider.spec.ts +++ b/packages/automated-dispute/tests/services/protocolProvider.spec.ts @@ -17,6 +17,7 @@ import { bondEscalationModuleAbi, eboRequestCreatorAbi, epochManagerAbi, + horizonAccountingExtensionAbi, oracleAbi, } from "../../src/abis/index.js"; import { @@ -59,15 +60,17 @@ describe("ProtocolProvider", () => { epochManager: "0x1234567890123456789012345678901234567890", eboRequestCreator: "0x1234567890123456789012345678901234567890", bondEscalationModule: "0x1234567890123456789012345678901234567890", + horizonAccountingExtension: "0x1234567890123456789012345678901234567890", }; beforeEach(() => { (getContract as Mock).mockImplementation(({ address, abi }) => { if (abi === oracleAbi && address === mockContractAddress.oracle) { - return {}; + return { address }; } if (abi === epochManagerAbi && address === mockContractAddress.epochManager) { return { + address, read: { currentEpoch: vi.fn(), currentEpochBlock: vi.fn(), @@ -76,6 +79,7 @@ describe("ProtocolProvider", () => { } if (abi === eboRequestCreatorAbi && address === mockContractAddress.eboRequestCreator) { return { + address, simulate: { createRequests: vi.fn(), }, @@ -89,6 +93,7 @@ describe("ProtocolProvider", () => { address === mockContractAddress.bondEscalationModule ) { return { + address, write: { pledgeForDispute: vi.fn(), pledgeAgainstDispute: vi.fn(), @@ -96,15 +101,31 @@ describe("ProtocolProvider", () => { }, }; } + if ( + abi === horizonAccountingExtensionAbi && + address === mockContractAddress.horizonAccountingExtension + ) { + return { + address, + read: { + approvedModules: vi.fn(), + }, + write: { + approveModule: vi.fn(), + }, + }; + } throw new Error("Invalid contract address or ABI"); }); (createPublicClient as Mock).mockImplementation(() => ({ - simulateContract: vi.fn().mockResolvedValue({ - request: { - functionName: "createRequests", - args: [], - }, + simulateContract: vi.fn().mockImplementation(({ functionName, args }) => { + return Promise.resolve({ + request: { + functionName, + args, + }, + }); }), getBlock: vi.fn(), waitForTransactionReceipt: vi.fn().mockResolvedValue({ status: "success" }), @@ -533,13 +554,6 @@ describe("ProtocolProvider", () => { }); describe("createRequest", () => { - const mockContractAddress: ProtocolContractsAddresses = { - oracle: "0x1234567890123456789012345678901234567890", - epochManager: "0x1234567890123456789012345678901234567890", - eboRequestCreator: "0x1234567890123456789012345678901234567890", - bondEscalationModule: "0x1234567890123456789012345678901234567890", - }; - it("creates a request successfully", async () => { const protocolProvider = new ProtocolProvider( mockRpcConfig, @@ -558,7 +572,7 @@ describe("ProtocolProvider", () => { await protocolProvider.createRequest(mockEpoch, mockChains); expect(protocolProvider["readClient"].simulateContract).toHaveBeenCalledWith({ - address: undefined, + address: mockContractAddress.eboRequestCreator, abi: eboRequestCreatorAbi, functionName: "createRequests", args: [mockEpoch, mockChains], @@ -568,7 +582,7 @@ describe("ProtocolProvider", () => { expect(protocolProvider["writeClient"].writeContract).toHaveBeenCalledWith( expect.objectContaining({ functionName: "createRequests", - args: [], + args: [mockEpoch, mockChains], }), ); }); @@ -581,6 +595,7 @@ describe("ProtocolProvider", () => { epochManager: "0x1234567890123456789012345678901234567890", eboRequestCreator: "0x1234567890123456789012345678901234567890", bondEscalationModule: "0x1234567890123456789012345678901234567890", + horizonAccountingExtension: "0x1234567890123456789012345678901234567890", }, mockedPrivateKey, ); @@ -739,4 +754,97 @@ describe("ProtocolProvider", () => { ).rejects.toThrow(TransactionExecutionError); }); }); + + describe("approveModule", () => { + it("successfully approves a module", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const mockModuleAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + + await expect(protocolProvider.approveModule(mockModuleAddress)).resolves.not.toThrow(); + + expect(protocolProvider["readClient"].simulateContract).toHaveBeenCalledWith({ + address: mockContractAddress.horizonAccountingExtension, + abi: horizonAccountingExtensionAbi, + functionName: "approveModule", + args: [mockModuleAddress], + account: expect.any(Object), + }); + + expect(protocolProvider["writeClient"].writeContract).toHaveBeenCalledWith( + expect.objectContaining({ + functionName: "approveModule", + args: [mockModuleAddress], + }), + ); + }); + + it("throws TransactionExecutionError when transaction fails", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + (protocolProvider["readClient"].waitForTransactionReceipt as Mock).mockResolvedValue({ + status: "reverted", + }); + + const mockModuleAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + + await expect(protocolProvider.approveModule(mockModuleAddress)).rejects.toThrow( + TransactionExecutionError, + ); + }); + }); + + describe("approvedModules", () => { + it("successfully retrieves approved modules for a user", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const mockUserAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + const mockApprovedModules = [ + "0x1111111111111111111111111111111111111111", + "0x2222222222222222222222222222222222222222", + ]; + + ( + protocolProvider["horizonAccountingExtensionContract"].read.approvedModules as Mock + ).mockResolvedValue(mockApprovedModules); + + const result = await protocolProvider.getApprovedModules(mockUserAddress); + + expect(result).toEqual(mockApprovedModules); + expect( + protocolProvider["horizonAccountingExtensionContract"].read.approvedModules, + ).toHaveBeenCalledWith([mockUserAddress]); + }); + + it("throws error when RPC client fails", async () => { + const protocolProvider = new ProtocolProvider( + mockRpcConfig, + mockContractAddress, + mockedPrivateKey, + ); + + const mockUserAddress = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + const error = new Error("RPC client failed"); + + ( + protocolProvider["horizonAccountingExtensionContract"].read.approvedModules as Mock + ).mockRejectedValue(error); + + await expect(protocolProvider.getApprovedModules(mockUserAddress)).rejects.toThrow( + error, + ); + }); + }); });