Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add horizon accounting extension logic #53

Merged
merged 3 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh nice!

Mind adding an example address for this new field in apps/agent/config.example.yml?

}),
});

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,
);
});
});
});