Skip to content

Commit

Permalink
feat: add horizon accounting extension logic (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
jahabeebs authored Oct 2, 2024
1 parent c370b28 commit f87030f
Show file tree
Hide file tree
Showing 8 changed files with 628 additions and 15 deletions.
1 change: 1 addition & 0 deletions apps/agent/config.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ protocolProvider:
epochManager: "0x1234567890123456789012345678901234567890"
eboRequestCreator: "0x1234567890123456789012345678901234567890"
bondEscalationModule: "0x1234567890123456789012345678901234567890"
horizonAccountingExtension: "0x1234567890123456789012345678901234567890"

blockNumberService:
blockmetaConfig:
Expand Down
1 change: 1 addition & 0 deletions apps/agent/src/config/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const protocolProviderConfigSchema = z.object({
epochManager: addressSchema,
eboRequestCreator: addressSchema,
bondEscalationModule: addressSchema,
horizonAccountingExtension: addressSchema,
}),
});

Expand Down
430 changes: 430 additions & 0 deletions packages/automated-dispute/src/abis/horizonAccountingExtension.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/automated-dispute/src/abis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from "./oracle.js";
export * from "./epochManager.js";
export * from "./eboRequestCreator.js";
export * from "./bondEscalationModule.js";
export * from "./horizonAccountingExtension.js";
1 change: 1 addition & 0 deletions packages/automated-dispute/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const ProtocolContractsNames = [
"epochManager",
"eboRequestCreator",
"bondEscalationModule",
"horizonAccountingExtension",
] as const;
16 changes: 16 additions & 0 deletions packages/automated-dispute/src/interfaces/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@ export interface IReadProvider {
* @returns A promise that resolves with an array of approved modules.
*/
getAccountingApprovedModules(): Promise<Address[]>;

/**
* 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<Address[]>;
}

/**
Expand Down Expand Up @@ -161,6 +169,14 @@ export interface IWriteProvider {
* @param modules an array of addresses for the modules to be approved
*/
approveAccountingModules(modules: Address[]): Promise<void>;

/**
* 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<void>;
}

/**
Expand Down
55 changes: 55 additions & 0 deletions packages/automated-dispute/src/providers/protocolProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
bondEscalationModuleAbi,
eboRequestCreatorAbi,
epochManagerAbi,
horizonAccountingExtensionAbi,
oracleAbi,
} from "../abis/index.js";
import {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = {
Expand All @@ -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 = {
Expand All @@ -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),
};

/**
Expand Down Expand Up @@ -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<void>} A promise that resolves when the module is approved.
*/
async approveModule(module: Address): Promise<void> {
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<Address[]> {
return [...(await this.horizonAccountingExtensionContract.read.approvedModules([user]))];
}

async getAccountingApprovedModules(): Promise<Address[]> {
// TODO: implement actual method
return [];
Expand Down
138 changes: 123 additions & 15 deletions packages/automated-dispute/tests/services/protocolProvider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
bondEscalationModuleAbi,
eboRequestCreatorAbi,
epochManagerAbi,
horizonAccountingExtensionAbi,
oracleAbi,
} from "../../src/abis/index.js";
import {
Expand Down Expand Up @@ -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(),
Expand All @@ -76,6 +79,7 @@ describe("ProtocolProvider", () => {
}
if (abi === eboRequestCreatorAbi && address === mockContractAddress.eboRequestCreator) {
return {
address,
simulate: {
createRequests: vi.fn(),
},
Expand All @@ -89,22 +93,39 @@ describe("ProtocolProvider", () => {
address === mockContractAddress.bondEscalationModule
) {
return {
address,
write: {
pledgeForDispute: vi.fn(),
pledgeAgainstDispute: vi.fn(),
settleDispute: vi.fn(),
},
};
}
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" }),
Expand Down Expand Up @@ -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,
Expand All @@ -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],
Expand All @@ -568,7 +582,7 @@ describe("ProtocolProvider", () => {
expect(protocolProvider["writeClient"].writeContract).toHaveBeenCalledWith(
expect.objectContaining({
functionName: "createRequests",
args: [],
args: [mockEpoch, mockChains],
}),
);
});
Expand All @@ -581,6 +595,7 @@ describe("ProtocolProvider", () => {
epochManager: "0x1234567890123456789012345678901234567890",
eboRequestCreator: "0x1234567890123456789012345678901234567890",
bondEscalationModule: "0x1234567890123456789012345678901234567890",
horizonAccountingExtension: "0x1234567890123456789012345678901234567890",
},
mockedPrivateKey,
);
Expand Down Expand Up @@ -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,
);
});
});
});

0 comments on commit f87030f

Please sign in to comment.