From 57cd750734a2fc50f7e4a37402a0cb63a52671d3 Mon Sep 17 00:00:00 2001 From: JoeLim13 Date: Wed, 21 May 2025 15:44:11 +0800 Subject: [PATCH] feat: nego feature --- src/acpClient.ts | 46 +++++++++++++++++++++++++---- src/acpContractClient.ts | 7 +++++ src/acpJob.ts | 5 ++-- src/acpMessage.ts | 64 ++++++++++++++++++++++++++++++++++++++++ src/configs.ts | 3 +- 5 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 src/acpMessage.ts diff --git a/src/acpClient.ts b/src/acpClient.ts index 02838ce..b3736ea 100644 --- a/src/acpClient.ts +++ b/src/acpClient.ts @@ -1,10 +1,15 @@ import { Address, parseEther } from "viem"; import { io } from "socket.io-client"; -import AcpContractClient, { AcpJobPhases, MemoType } from "./acpContractClient"; +import AcpContractClient, { + AcpJobPhases, + AcpNegoStatus, + MemoType, +} from "./acpContractClient"; import { AcpAgent } from "../interfaces"; import AcpJob from "./acpJob"; import AcpMemo from "./acpMemo"; import AcpJobOffering from "./acpJobOffering"; +import AcpMessage from "./acpMessage"; export interface IDeliverable { type: string; @@ -29,6 +34,7 @@ interface IAcpJob { data: { onChainJobId: number; phase: AcpJobPhases; + negoStatus: AcpNegoStatus; description: string; buyerAddress: `0x${string}`; sellerAddress: `0x${string}`; @@ -57,12 +63,15 @@ interface IAcpClientOptions { acpContractClient: AcpContractClient; onNewTask?: (job: AcpJob) => void; onEvaluate?: (job: AcpJob) => void; + onNewMsg?: (msg: AcpMessage, job: AcpJob) => void; } -enum SocketEvents { +export enum SocketEvents { ROOM_JOINED = "roomJoined", ON_EVALUATE = "onEvaluate", ON_NEW_TASK = "onNewTask", + ON_NEW_MSG = "onNewMsg", + ON_CREATE_MSG = "onCreateMsg", } export class EvaluateResult { isApproved: boolean; @@ -76,15 +85,16 @@ export class EvaluateResult { class AcpClient { private acpUrl; + private acpJob: AcpJob | null = null; public acpContractClient: AcpContractClient; private onNewTask?: (job: AcpJob) => void; private onEvaluate?: (job: AcpJob) => void; - + private onNewMsg?: (msg: AcpMessage, job: AcpJob) => void; constructor(options: IAcpClientOptions) { this.acpContractClient = options.acpContractClient; this.onNewTask = options.onNewTask; this.onEvaluate = options.onEvaluate || this.defaultOnEvaluate; - + this.onNewMsg = options.onNewMsg; this.acpUrl = this.acpContractClient.config.acpUrl; this.init(); } @@ -129,9 +139,12 @@ class AcpClient { memo.nextPhase ); }), - data.phase + data.phase, + data.negoStatus ); + this.acpJob = job; + this.onEvaluate(job); } } @@ -156,14 +169,35 @@ class AcpClient { memo.nextPhase ); }), - data.phase + data.phase, + data.negoStatus ); + this.acpJob = job; + this.onNewTask(job); } } ); + socket.on(SocketEvents.ON_NEW_MSG, (data, callback) => { + callback(true); + + if (this.onNewMsg && this.acpJob) { + this.acpJob.negoStatus = AcpNegoStatus.PENDING; + + const msg = new AcpMessage( + data.id, + data.messages ?? [], + socket, + this.acpJob, + this.acpContractClient.walletAddress + ); + + this.onNewMsg(msg, this.acpJob); + } + }); + const cleanup = async () => { if (socket) { socket.disconnect(); diff --git a/src/acpContractClient.ts b/src/acpContractClient.ts index 4ce9752..026bc6e 100644 --- a/src/acpContractClient.ts +++ b/src/acpContractClient.ts @@ -26,6 +26,13 @@ export enum AcpJobPhases { REJECTED = 5, } +export enum AcpNegoStatus { + PENDING = "PENDING", + AGREED = "AGREED", + DISAGREED = "DISAGREED", + NOT_STARTED = "NOT_STARTED", +} + class AcpContractClient { private _sessionKeyClient: ModularAccountV2Client | undefined; diff --git a/src/acpJob.ts b/src/acpJob.ts index eee869a..1935ff9 100644 --- a/src/acpJob.ts +++ b/src/acpJob.ts @@ -1,5 +1,5 @@ import AcpClient from "./acpClient"; -import { AcpJobPhases } from "./acpContractClient"; +import { AcpJobPhases, AcpNegoStatus } from "./acpContractClient"; import AcpMemo from "./acpMemo"; class AcpJob { @@ -8,7 +8,8 @@ class AcpJob { public id: number, public providerAddress: string, public memos: AcpMemo[], - public phase: AcpJobPhases + public phase: AcpJobPhases, + public negoStatus: AcpNegoStatus ) {} async pay(amount: number, reason?: string) { diff --git a/src/acpMessage.ts b/src/acpMessage.ts new file mode 100644 index 0000000..0cbb2be --- /dev/null +++ b/src/acpMessage.ts @@ -0,0 +1,64 @@ +import { Socket } from "socket.io-client"; +import { SocketEvents } from "./acpClient"; +import AcpJob from "./acpJob"; +import { AcpJobPhases, AcpNegoStatus } from "./acpContractClient"; +import { Address } from "viem"; + +interface Message { + id: number; + sender: Address; + recipient: Address; + content: string; + timestamp: number; +} + +class AcpMessage { + constructor( + public id: number, + public messages: Message[], + public socket: Socket, + public acpJob: AcpJob | null, + public walletAddress: Address + ) {} + + initOrReply(message: string) { + if (!this.acpJob) { + throw new Error("Cannot initiate or reply conversation without job"); + } + + if (this.acpJob.negoStatus !== AcpNegoStatus.PENDING) { + throw new Error( + "Cannot initiate or reply conversation in non-negotiation phase" + ); + } + + if ( + this.messages.length > 0 && + this.messages[this.messages.length - 1].sender === this.walletAddress + ) { + throw new Error("Cannot reply to own message"); + } + + this.socket.timeout(5000).emit( + SocketEvents.ON_CREATE_MSG, + { + jobId: this.acpJob.id, + content: message, + sender: this.walletAddress, + recipient: + this.messages.length > 0 + ? this.messages[this.messages.length - 1].sender + : this.acpJob.providerAddress, + }, + (err: any, response: any) => { + if (err || !response) { + console.log(`Message not received`, err); + } else { + console.log(`Message received`); + } + } + ); + } +} + +export default AcpMessage; diff --git a/src/configs.ts b/src/configs.ts index a7f09a3..f30dd5a 100644 --- a/src/configs.ts +++ b/src/configs.ts @@ -12,7 +12,8 @@ const baseSepoliaAcpConfig: AcpContractConfig = { chain: baseSepolia, contractAddress: "0x2422c1c43451Eb69Ff49dfD39c4Dc8C5230fA1e6", virtualsTokenAddress: "0xbfAB80ccc15DF6fb7185f9498d6039317331846a", - acpUrl: "https://acpx-staging.virtuals.io", + acpUrl: "http://localhost:1337", + // acpUrl: "https://acpx-staging.virtuals.io", }; const baseAcpConfig: AcpContractConfig = {