-
Notifications
You must be signed in to change notification settings - Fork 0
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: build onRequestCreated #18
Changes from all commits
6cf15f3
6bb11e6
40f0e03
6c7e02e
c8d6702
03e75c7
bd3f0db
33271d7
6647610
4a16f80
be694f1
c9bca60
2aba0ac
a91e4ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,23 +1,121 @@ | ||
import { BlockNumberService } from "@ebo-agent/blocknumber"; | ||
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; | ||
import { ILogger } from "@ebo-agent/shared"; | ||
import { ContractFunctionRevertedError } from "viem"; | ||
|
||
import { InvalidActorState } from "./exceptions/invalidActorState.exception.js"; | ||
import { RequestMismatch } from "./exceptions/requestMismatch.js"; | ||
import { EboRegistry } from "./interfaces/eboRegistry.js"; | ||
import { ProtocolProvider } from "./protocolProvider.js"; | ||
import { EboEvent } from "./types/events.js"; | ||
import { Dispute, Response } from "./types/prophet.js"; | ||
|
||
export class EboActor { | ||
private requestActivity: unknown[]; | ||
|
||
constructor( | ||
private readonly protocolProvider: ProtocolProvider, | ||
private readonly blockNumberService: BlockNumberService, | ||
private readonly registry: EboRegistry, | ||
private readonly requestId: string, | ||
) { | ||
this.requestActivity = []; | ||
private readonly logger: ILogger, | ||
) {} | ||
|
||
/** | ||
* Handle RequestCreated event. | ||
* | ||
* @param event RequestCreated event | ||
*/ | ||
public async onRequestCreated(event: EboEvent<"RequestCreated">): Promise<void> { | ||
if (event.metadata.requestId != this.requestId) | ||
throw new RequestMismatch(this.requestId, event.metadata.requestId); | ||
|
||
if (this.registry.getRequest(event.metadata.requestId)) { | ||
this.logger.error( | ||
`The request ${event.metadata.requestId} was already being handled by an actor.`, | ||
); | ||
|
||
throw new InvalidActorState(); | ||
} | ||
|
||
this.registry.addRequest(event.metadata.requestId, event.metadata.request); | ||
|
||
if (this.anyActiveProposal()) { | ||
// Skipping new proposal until the actor receives a ResponseDisputed event; | ||
// at that moment, it will be possible to re-propose again. | ||
this.logger.info( | ||
`There is an active proposal for request ${this.requestId}. Skipping...`, | ||
); | ||
|
||
return; | ||
} | ||
|
||
const { chainId } = event.metadata; | ||
const { currentEpoch, currentEpochTimestamp } = | ||
await this.protocolProvider.getCurrentEpoch(); | ||
|
||
const epochBlockNumber = await this.blockNumberService.getEpochBlockNumber( | ||
currentEpochTimestamp, | ||
chainId, | ||
); | ||
|
||
if (this.alreadyProposed(currentEpoch, chainId, epochBlockNumber)) return; | ||
|
||
try { | ||
await this.protocolProvider.proposeResponse( | ||
this.requestId, | ||
currentEpoch, | ||
chainId, | ||
epochBlockNumber, | ||
); | ||
} catch (err) { | ||
if (err instanceof ContractFunctionRevertedError) { | ||
this.logger.warn( | ||
`Block ${epochBlockNumber} for epoch ${currentEpoch} and ` + | ||
`chain ${chainId} was not proposed. Skipping proposal...`, | ||
); | ||
} else { | ||
this.logger.error( | ||
`Actor handling request ${this.requestId} is not able to continue.`, | ||
); | ||
|
||
throw err; | ||
} | ||
} | ||
} | ||
|
||
public async onRequestCreated(_event: EboEvent<"RequestCreated">): Promise<void> { | ||
// TODO: implement | ||
return; | ||
/** | ||
* Check if there's at least one proposal that has not received any dispute yet. | ||
* | ||
* @returns | ||
*/ | ||
private anyActiveProposal() { | ||
// TODO: implement this function | ||
return false; | ||
} | ||
|
||
/** | ||
* Check if the same proposal has already been made in the past. | ||
* | ||
* @param epoch epoch of the request | ||
* @param chainId chain id of the request | ||
* @param blockNumber proposed block number | ||
* @returns true if there's a registry of a proposal with the same attributes, false otherwise | ||
*/ | ||
private alreadyProposed(epoch: bigint, chainId: Caip2ChainId, blockNumber: bigint) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would call this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also , we should add an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it make sense to have a new propose with the same block? I'd think that if there has already been a proposal with the block N, rejected or not, the agent wouldn't want to propose the same block N again. I might be missing something though. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Left this as |
||
const responses = this.registry.getResponses(); | ||
|
||
for (const [responseId, response] of responses) { | ||
if (response.response.block != blockNumber) continue; | ||
if (response.response.chainId != chainId) continue; | ||
if (response.response.epoch != epoch) continue; | ||
|
||
this.logger.info( | ||
`Block ${blockNumber} for epoch ${epoch} and chain ${chainId} already proposed on response ${responseId}. Skipping...`, | ||
); | ||
|
||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
public async onResponseProposed(_event: EboEvent<"ResponseDisputed">): Promise<void> { | ||
|
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,25 @@ | ||||||||
import { EboRegistry } from "./interfaces/eboRegistry.js"; | ||||||||
import { Dispute, Request, Response } from "./types/prophet.js"; | ||||||||
|
||||||||
export class EboMemoryRegistry implements EboRegistry { | ||||||||
constructor( | ||||||||
private requests: Map<string, Request> = new Map(), | ||||||||
private responses: Map<string, Response> = new Map(), | ||||||||
private dispute: Map<string, Dispute> = new Map(), | ||||||||
) {} | ||||||||
|
||||||||
/** @inheritdoc */ | ||||||||
public addRequest(requestId: string, request: Request) { | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ooaa nice, didn't know that. however, vscode is so magical that doesn't need this tag xd |
||||||||
this.requests.set(requestId, request); | ||||||||
} | ||||||||
|
||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
/** @inheritdoc */ | ||||||||
public getRequest(requestId: string) { | ||||||||
return this.requests.get(requestId); | ||||||||
} | ||||||||
|
||||||||
/** @inheritdoc */ | ||||||||
public getResponses() { | ||||||||
return this.responses; | ||||||||
} | ||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,2 @@ | ||
export * from "./rpcUrlsEmpty.exception.js"; | ||
export * from "./invalidActorState.exception.js"; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
export class InvalidActorState extends Error { | ||
constructor() { | ||
// TODO: we'll want to dump the Actor state into stderr at this point | ||
super("The actor is in an invalid state."); | ||
|
||
this.name = "InvalidActorState"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export class RequestMismatch extends Error { | ||
constructor(requestId: string, eventRequestId: string) { | ||
super(`Actor handling request ${requestId} received a request ${eventRequestId} event.`); | ||
this.name = "RequestMismatch"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Request, Response } from "../types/prophet.js"; | ||
|
||
/** Registry that stores Prophet entities (ie. requests, responses and disputes) */ | ||
export interface EboRegistry { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lets add a high level description here |
||
/** | ||
* Add a `Request` by ID. | ||
* | ||
* @param requestId the ID of the `Request` | ||
* @param request the `Request` | ||
*/ | ||
addRequest(requestId: string, request: Request): void; | ||
|
||
/** | ||
* Get a `Request` by ID. | ||
* | ||
* @param requestId request ID | ||
* @returns the request if already added into registry, `undefined` otherwise | ||
*/ | ||
getRequest(requestId: string): Request | undefined; | ||
|
||
/** | ||
* Return all responses | ||
* | ||
* @returns responses map | ||
*/ | ||
getResponses(): Map<string, Response>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
import { Caip2ChainId } from "@ebo-agent/blocknumber/dist/types.js"; | ||
import { Log } from "viem"; | ||
|
||
import { Dispute, Request } from "./prophet.js"; | ||
import { Dispute, Request, Response } from "./prophet.js"; | ||
|
||
export type EboEventName = | ||
| "NewEpoch" | ||
|
@@ -17,14 +18,17 @@ export interface NewEpoch { | |
epochBlockNumber: bigint; | ||
} | ||
|
||
export interface ResponseCreated { | ||
export interface ResponseProposed { | ||
requestId: string; | ||
request: Request; | ||
responseId: string; | ||
response: Response; | ||
} | ||
|
||
export interface RequestCreated { | ||
requestId: string; | ||
epoch: bigint; | ||
chainId: Caip2ChainId; | ||
request: Request; | ||
requestId: string; | ||
} | ||
|
||
export interface ResponseDisputed { | ||
|
@@ -60,8 +64,8 @@ export type EboEventData<E extends EboEventName> = E extends "NewEpoch" | |
? NewEpoch | ||
: E extends "RequestCreated" | ||
? RequestCreated | ||
: E extends "ResponseCreated" | ||
? ResponseCreated | ||
: E extends "ResponseProposed" | ||
? ResponseProposed | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤣 |
||
: E extends "ResponseDisputed" | ||
? ResponseDisputed | ||
: E extends "DisputeStatusChanged" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@0xkenj1 included this check prior starting to build the
Response
(any response will be ignored if there's an active proposal)