Skip to content

Commit c5d00e4

Browse files
Merge pull request #147 from Virtual-Protocol/feat/acp-1033
feat: cross chain transfer service
2 parents 393eb59 + 166d2f6 commit c5d00e4

16 files changed

Lines changed: 1926 additions & 209 deletions

File tree

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import logging
2+
import threading
3+
import os
4+
import sys
5+
6+
# Add project root to Python path
7+
project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../"))
8+
sys.path.insert(0, project_root)
9+
10+
from datetime import datetime, timedelta
11+
from typing import Optional
12+
13+
from dotenv import load_dotenv
14+
15+
from virtuals_acp.client import VirtualsACP
16+
from virtuals_acp.configs.configs import BASE_MAINNET_ACP_X402_CONFIG_V2, BASE_SEPOLIA_ACP_X402_CONFIG_V2
17+
from virtuals_acp.contract_clients.contract_client_v2 import ACPContractClientV2
18+
from virtuals_acp.env import EnvSettings
19+
from virtuals_acp.job import ACPJob
20+
from virtuals_acp.memo import ACPMemo
21+
from virtuals_acp.models import (
22+
ACPAgentSort,
23+
ACPJobPhase,
24+
ACPGraduationStatus,
25+
ACPOnlineStatus,
26+
ChainConfig
27+
)
28+
29+
# Configure logging
30+
logging.basicConfig(
31+
level=logging.INFO,
32+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
33+
)
34+
logger = logging.getLogger("BuyerAgent")
35+
36+
load_dotenv(override=True)
37+
38+
TARGET_CHAIN_ID = 97
39+
40+
config = BASE_SEPOLIA_ACP_X402_CONFIG_V2
41+
config.chains = [
42+
ChainConfig(
43+
chain_id=TARGET_CHAIN_ID,
44+
rpc_url="https://bsc-testnet-dataseed.bnbchain.org"
45+
)
46+
]
47+
48+
49+
def buyer():
50+
env = EnvSettings()
51+
52+
def on_new_task(job: ACPJob, memo_to_sign: Optional[ACPMemo] = None):
53+
if (
54+
job.phase == ACPJobPhase.NEGOTIATION
55+
and memo_to_sign is not None
56+
and memo_to_sign.next_phase == ACPJobPhase.TRANSACTION
57+
):
58+
logger.info(f"Paying for job {job.id}")
59+
job.pay_and_accept_requirement()
60+
logger.info(f"Job {job.id} paid")
61+
62+
elif (
63+
job.phase == ACPJobPhase.TRANSACTION
64+
and memo_to_sign is not None
65+
and memo_to_sign.next_phase == ACPJobPhase.REJECTED
66+
):
67+
logger.info(f"Signing job {job.id} rejection memo, rejection reason: {memo_to_sign.content}")
68+
memo_to_sign.sign(True, "Accepts job rejection")
69+
logger.info(f"Job {job.id} rejection memo signed")
70+
71+
elif job.phase == ACPJobPhase.COMPLETED:
72+
logger.info(f"Job {job.id} completed, received deliverable: {job.deliverable}")
73+
74+
elif job.phase == ACPJobPhase.REJECTED:
75+
logger.info(f"Job {job.id} rejected by seller")
76+
77+
acp_client = VirtualsACP(
78+
acp_contract_clients=ACPContractClientV2(
79+
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY,
80+
agent_wallet_address=env.BUYER_AGENT_WALLET_ADDRESS,
81+
entity_id=env.BUYER_ENTITY_ID,
82+
config=config, # route to x402 for payment, undefined defaulted back to direct transfer
83+
),
84+
on_new_task=on_new_task
85+
)
86+
87+
# Browse available agents based on a keyword
88+
relevant_agents = acp_client.browse_agents(
89+
keyword="cross chain transfer service",
90+
sort_by=[ACPAgentSort.SUCCESSFUL_JOB_COUNT],
91+
top_k=5,
92+
graduation_status=ACPGraduationStatus.ALL,
93+
online_status=ACPOnlineStatus.ALL,
94+
show_hidden_offerings=True,
95+
)
96+
logger.info(f"Relevant agents: {relevant_agents}")
97+
98+
# Pick one of the agents based on your criteria (in this example we just pick the first one)
99+
chosen_agent = relevant_agents[0]
100+
# Pick one of the service offerings based on your criteria (in this example we just pick the first one)
101+
chosen_job_offering = chosen_agent.job_offerings[1]
102+
103+
job_id = chosen_job_offering.initiate_job(
104+
service_requirement={},
105+
expired_at=datetime.now() + timedelta(minutes=5), # job expiry duration, minimum 3 minutes
106+
)
107+
logger.info(f"Job {job_id} initiated")
108+
logger.info("Listening for next steps...")
109+
110+
threading.Event().wait()
111+
112+
113+
if __name__ == "__main__":
114+
buyer()
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import logging
2+
import threading
3+
4+
import sys
5+
sys.path.append("../../../")
6+
7+
from typing import Optional
8+
from dotenv import load_dotenv
9+
10+
from virtuals_acp.client import VirtualsACP
11+
from virtuals_acp.configs.configs import BASE_SEPOLIA_ACP_X402_CONFIG_V2
12+
from virtuals_acp.contract_clients.contract_client_v2 import ACPContractClientV2
13+
from virtuals_acp.env import EnvSettings
14+
from virtuals_acp.fare import Fare, FareAmount
15+
from virtuals_acp.job import ACPJob
16+
from virtuals_acp.memo import ACPMemo
17+
from virtuals_acp.models import ACPJobPhase, ChainConfig, MemoType
18+
19+
# Configure logging
20+
logging.basicConfig(
21+
level=logging.INFO,
22+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
23+
)
24+
logger = logging.getLogger("SellerAgent")
25+
26+
load_dotenv(override=True)
27+
28+
REJECT_JOB_IN_REQUEST_PHASE = False
29+
REJECT_JOB_IN_OTHER_PHASE = False
30+
SOURCE_TOKEN_ADDRESS = ""
31+
TARGET_TOKEN_ADDRESS = ""
32+
TARGET_CHAIN_ID = 97
33+
34+
config = BASE_SEPOLIA_ACP_X402_CONFIG_V2
35+
config.chains = [
36+
ChainConfig(
37+
chain_id=TARGET_CHAIN_ID,
38+
rpc_url="https://bsc-testnet-dataseed.bnbchain.org"
39+
)
40+
]
41+
42+
def seller():
43+
env = EnvSettings()
44+
45+
def on_new_task(job: ACPJob, memo_to_sign: Optional[ACPMemo] = None):
46+
logger.info(f"[on_new_task] Received job {job.id} (phase: {job.phase})")
47+
48+
if (
49+
job.phase == ACPJobPhase.REQUEST
50+
and memo_to_sign is not None
51+
and memo_to_sign.next_phase == ACPJobPhase.NEGOTIATION
52+
):
53+
logger.info(f"Responding to job {job.id} with requirement: {job.requirement}")
54+
if REJECT_JOB_IN_REQUEST_PHASE:
55+
job.reject("Job requirement does not meet agent capability")
56+
else:
57+
job.accept("Job requirement matches agent capability")
58+
59+
swappedToken = FareAmount(
60+
1,
61+
Fare.from_contract_address(
62+
TARGET_TOKEN_ADDRESS,
63+
config,
64+
TARGET_CHAIN_ID
65+
)
66+
)
67+
68+
job.create_payable_requirement(
69+
"Requesting token from client on destination chain",
70+
MemoType.PAYABLE_REQUEST,
71+
swappedToken,
72+
job.client_address,
73+
)
74+
75+
logger.info(f"Job {job.id} responded with {'rejected' if REJECT_JOB_IN_REQUEST_PHASE else 'accepted'}")
76+
77+
elif (
78+
job.phase == ACPJobPhase.TRANSACTION
79+
and memo_to_sign is not None
80+
and memo_to_sign.next_phase == ACPJobPhase.EVALUATION
81+
):
82+
# to cater cases where agent decide to reject job after payment has been made
83+
if REJECT_JOB_IN_OTHER_PHASE: # conditional check for job rejection logic
84+
reason = "Job requirement does not meet agent capability"
85+
logger.info(f"Rejecting job {job.id} with reason: {reason}")
86+
job.reject(reason)
87+
logger.info(f"Job {job.id} rejected")
88+
return
89+
90+
deliverable = FareAmount(
91+
1,
92+
Fare.from_contract_address(
93+
TARGET_TOKEN_ADDRESS,
94+
config,
95+
TARGET_CHAIN_ID
96+
)
97+
)
98+
logger.info(f"Delivering job {job.id} with deliverable {deliverable}")
99+
# job.deliver(deliverable)
100+
job.deliver_payable("Deliver swapped token on destination chain", deliverable)
101+
logger.info(f"Job {job.id} delivered")
102+
return
103+
104+
elif job.phase == ACPJobPhase.COMPLETED:
105+
logger.info(f"Job {job.id} completed")
106+
107+
elif job.phase == ACPJobPhase.REJECTED:
108+
logger.info(f"Job {job.id} rejected")
109+
110+
VirtualsACP(
111+
acp_contract_clients=ACPContractClientV2(
112+
wallet_private_key=env.WHITELISTED_WALLET_PRIVATE_KEY,
113+
agent_wallet_address=env.SELLER_AGENT_WALLET_ADDRESS,
114+
entity_id=env.SELLER_ENTITY_ID,
115+
config=config
116+
),
117+
on_new_task=on_new_task
118+
)
119+
120+
logger.info("Seller agent is running, waiting for new tasks...")
121+
threading.Event().wait()
122+
123+
124+
if __name__ == "__main__":
125+
seller()

0 commit comments

Comments
 (0)