Skip to content

Commit 648f484

Browse files
authored
Merge pull request #132 from Virtual-Protocol/feat/yang-check-acp-agent-env-correctness
feat: add read contract check on session signer
2 parents 906556f + 5105f9b commit 648f484

11 files changed

Lines changed: 335 additions & 30 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "virtuals-acp"
3-
version = "0.3.14"
3+
version = "0.3.15"
44
description = "Agent Commerce Protocol Python SDK by Virtuals"
55
authors = ["Steven Lee Soon Fatt <steven@virtuals.io>"]
66
readme = "README.md"
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
SINGLE_SIGNER_VALIDATION_MODULE_ABI = [
2+
{ "inputs": [], "name": "InvalidSignatureType", "type": "error" },
3+
{
4+
"inputs": [],
5+
"name": "NotAuthorized",
6+
"type": "error",
7+
},
8+
{ "inputs": [], "name": "NotImplemented", "type": "error" },
9+
{
10+
"inputs": [],
11+
"name": "UnexpectedDataPassed",
12+
"type": "error",
13+
},
14+
{
15+
"anonymous": True,
16+
"inputs": [
17+
{
18+
"indexed": True,
19+
"internalType": "address",
20+
"name": "account",
21+
"type": "address",
22+
},
23+
{
24+
"indexed": True,
25+
"internalType": "uint32",
26+
"name": "entityId",
27+
"type": "uint32",
28+
},
29+
{
30+
"indexed": True,
31+
"internalType": "address",
32+
"name": "newSigner",
33+
"type": "address",
34+
},
35+
{
36+
"indexed": False,
37+
"internalType": "address",
38+
"name": "previousSigner",
39+
"type": "address",
40+
},
41+
],
42+
"name": "SignerTransferred",
43+
"type": "event",
44+
},
45+
{
46+
"inputs": [],
47+
"name": "moduleId",
48+
"outputs": [{ "internalType": "string", "name": "", "type": "string" }],
49+
"stateMutability": "pure",
50+
"type": "function",
51+
},
52+
{
53+
"inputs": [{ "internalType": "bytes", "name": "data", "type": "bytes" }],
54+
"name": "onInstall",
55+
"outputs": [],
56+
"stateMutability": "nonpayable",
57+
"type": "function",
58+
},
59+
{
60+
"inputs": [{ "internalType": "bytes", "name": "data", "type": "bytes" }],
61+
"name": "onUninstall",
62+
"outputs": [],
63+
"stateMutability": "nonpayable",
64+
"type": "function",
65+
},
66+
{
67+
"inputs": [
68+
{ "internalType": "address", "name": "account", "type": "address" },
69+
{
70+
"internalType": "bytes32",
71+
"name": "hash",
72+
"type": "bytes32",
73+
},
74+
],
75+
"name": "replaySafeHash",
76+
"outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }],
77+
"stateMutability": "view",
78+
"type": "function",
79+
},
80+
{
81+
"inputs": [
82+
{ "internalType": "uint32", "name": "entityId", "type": "uint32" },
83+
{
84+
"internalType": "address",
85+
"name": "account",
86+
"type": "address",
87+
},
88+
],
89+
"name": "signers",
90+
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
91+
"stateMutability": "view",
92+
"type": "function",
93+
},
94+
{
95+
"inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }],
96+
"name": "supportsInterface",
97+
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
98+
"stateMutability": "view",
99+
"type": "function",
100+
},
101+
{
102+
"inputs": [
103+
{ "internalType": "uint32", "name": "entityId", "type": "uint32" },
104+
{
105+
"internalType": "address",
106+
"name": "newSigner",
107+
"type": "address",
108+
},
109+
],
110+
"name": "transferSigner",
111+
"outputs": [],
112+
"stateMutability": "nonpayable",
113+
"type": "function",
114+
},
115+
{
116+
"inputs": [
117+
{ "internalType": "address", "name": "account", "type": "address" },
118+
{
119+
"internalType": "uint32",
120+
"name": "entityId",
121+
"type": "uint32",
122+
},
123+
{ "internalType": "address", "name": "sender", "type": "address" },
124+
{
125+
"internalType": "uint256",
126+
"name": "",
127+
"type": "uint256",
128+
},
129+
{ "internalType": "bytes", "name": "", "type": "bytes" },
130+
{
131+
"internalType": "bytes",
132+
"name": "",
133+
"type": "bytes",
134+
},
135+
],
136+
"name": "validateRuntime",
137+
"outputs": [],
138+
"stateMutability": "view",
139+
"type": "function",
140+
},
141+
{
142+
"inputs": [
143+
{ "internalType": "address", "name": "account", "type": "address" },
144+
{
145+
"internalType": "uint32",
146+
"name": "entityId",
147+
"type": "uint32",
148+
},
149+
{ "internalType": "address", "name": "", "type": "address" },
150+
{
151+
"internalType": "bytes32",
152+
"name": "digest",
153+
"type": "bytes32",
154+
},
155+
{ "internalType": "bytes", "name": "signature", "type": "bytes" },
156+
],
157+
"name": "validateSignature",
158+
"outputs": [{ "internalType": "bytes4", "name": "", "type": "bytes4" }],
159+
"stateMutability": "view",
160+
"type": "function",
161+
},
162+
{
163+
"inputs": [
164+
{
165+
"internalType": "uint32",
166+
"name": "entityId",
167+
"type": "uint32",
168+
},
169+
{
170+
"components": [
171+
{ "internalType": "address", "name": "sender", "type": "address" },
172+
{
173+
"internalType": "uint256",
174+
"name": "nonce",
175+
"type": "uint256",
176+
},
177+
{ "internalType": "bytes", "name": "initCode", "type": "bytes" },
178+
{
179+
"internalType": "bytes",
180+
"name": "callData",
181+
"type": "bytes",
182+
},
183+
{
184+
"internalType": "bytes32",
185+
"name": "accountGasLimits",
186+
"type": "bytes32",
187+
},
188+
{
189+
"internalType": "uint256",
190+
"name": "preVerificationGas",
191+
"type": "uint256",
192+
},
193+
{ "internalType": "bytes32", "name": "gasFees", "type": "bytes32" },
194+
{
195+
"internalType": "bytes",
196+
"name": "paymasterAndData",
197+
"type": "bytes",
198+
},
199+
{ "internalType": "bytes", "name": "signature", "type": "bytes" },
200+
],
201+
"internalType": "struct PackedUserOperation",
202+
"name": "userOp",
203+
"type": "tuple",
204+
},
205+
{ "internalType": "bytes32", "name": "userOpHash", "type": "bytes32" },
206+
],
207+
"name": "validateUserOp",
208+
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
209+
"stateMutability": "view",
210+
"type": "function",
211+
},
212+
]

virtuals_acp/alchemy.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import secrets
22
import time
3-
from typing import Dict, Any, List, Optional, Union
43
from dataclasses import dataclass
54
from enum import Enum
5+
from typing import Dict, Any, List, Optional, Union
66

77
import requests
88
from eth_account import Account
9+
from eth_account.messages import encode_defunct
910
from eth_account.messages import encode_typed_data
1011
from eth_utils.conversions import to_hex
11-
from eth_account.messages import encode_defunct
12-
1312

1413
from virtuals_acp.configs.configs import BASE_SEPOLIA_CONFIG, ACPContractConfig
1514
from virtuals_acp.models import OperationPayload

virtuals_acp/configs/configs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# virtuals_acp/configs.py
2-
from typing import Literal, Optional, Union, List, Dict, Any
2+
from web3 import Web3
3+
4+
from typing import Literal, Optional, List, Dict, Any
35
from virtuals_acp.fare import Fare
46
from virtuals_acp.abis.abi import ACP_ABI
57
from virtuals_acp.abis.abi_v2 import ACP_V2_ABI

virtuals_acp/constants.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from web3 import Web3
2+
13
USDC_TOKEN_ADDRESS = {
24
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
35
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
@@ -13,6 +15,6 @@
1315
{"name": "nonce", "type": "bytes32"},
1416
]
1517

16-
VERIFYING_CONTRACT_ADDRESS = "0x00000000000099DE0BF6fA90dEB851E2A2df7d83"
17-
1818
HTTP_STATUS_CODES_X402 = {"Payment Required": 402, "OK": 200}
19+
20+
SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS = Web3.to_checksum_address("0x00000000000099DE0BF6fA90dEB851E2A2df7d83")

virtuals_acp/contract_clients/base_contract_client.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1+
import json
12
from abc import ABC, abstractmethod
23
from datetime import datetime
34
from decimal import Decimal
45
import math
56
from typing import Dict, Any, Optional, List, cast
67

78
from eth_typing import ABIEvent
8-
import requests
9+
from ens.utils import is_none_or_zero_address
910
from web3 import Web3
1011
from web3.contract import Contract
1112
from eth_utils.abi import event_abi_to_log_topic
1213

1314
from virtuals_acp.abis.erc20_abi import ERC20_ABI
1415
from virtuals_acp.abis.flat_token_v2_abi import FIAT_TOKEN_V2_ABI
16+
from virtuals_acp.abis.single_signer_validation_module_abi import SINGLE_SIGNER_VALIDATION_MODULE_ABI
1517
from virtuals_acp.abis.weth_abi import WETH_ABI
18+
from virtuals_acp.constants import SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS
1619
from virtuals_acp.fare import WETH_FARE
1720
from virtuals_acp.configs.configs import ACPContractConfig
1821
from virtuals_acp.exceptions import ACPError
@@ -26,7 +29,6 @@
2629
X402PayableRequirements,
2730
OperationPayload,
2831
OffChainJob,
29-
X402PaymentResponse,
3032
)
3133

3234

@@ -67,9 +69,58 @@ def __init__(self, agent_wallet_address: str, config: ACPContractConfig):
6769
self.job_created_event_signature_hex = (
6870
"0x" + event_abi_to_log_topic(cast(ABIEvent, job_created_event_abi)).hex()
6971
)
70-
72+
73+
agent_smart_contract_code = self.w3.eth.get_code(Web3.to_checksum_address(self.agent_wallet_address))
74+
is_account_deployed = len(agent_smart_contract_code) > 0
75+
if not is_account_deployed:
76+
raise ACPError(f"ACP Contract Client validation failed: agent account {self.agent_wallet_address} is not deployed on-chain")
77+
78+
def validate_session_key_on_chain(
79+
self,
80+
session_signer_address: str,
81+
session_entity_key_id: int
82+
) -> None:
83+
single_signer_validation_contract: Contract = self.w3.eth.contract(
84+
address=SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS,
85+
abi=SINGLE_SIGNER_VALIDATION_MODULE_ABI,
86+
)
87+
on_chain_signer_address = (
88+
single_signer_validation_contract
89+
.functions
90+
.signers(session_entity_key_id, self.agent_wallet_address)
91+
.call()
92+
)
93+
94+
if is_none_or_zero_address(on_chain_signer_address):
95+
raise ACPError(
96+
"ACP Contract Client validation failed:\n" +
97+
json.dumps(
98+
{
99+
"reason": "no whitelisted wallet registered on-chain for entity id",
100+
"entity_id": session_entity_key_id,
101+
"agent_wallet_address": self.agent_wallet_address,
102+
},
103+
indent=2
104+
)
105+
)
106+
107+
if on_chain_signer_address.lower() != session_signer_address.lower():
108+
raise ACPError(
109+
"ACP Contract Client validation failed:\n" +
110+
json.dumps(
111+
{
112+
"agent_wallet_address": self.agent_wallet_address,
113+
"entity_id": session_entity_key_id,
114+
"given_whitelisted_wallet_address": session_signer_address,
115+
"expected_whitelisted_wallet_address": on_chain_signer_address,
116+
"reason": "session signer address mismatch",
117+
},
118+
indent=2
119+
)
120+
)
121+
71122
@abstractmethod
72-
def getAcpVersion(self) -> str:
123+
def get_acp_version(self) -> str:
73124
pass
74125

75126
def _build_user_operation(

0 commit comments

Comments
 (0)