Skip to content
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
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "virtuals-acp"
version = "0.3.14"
version = "0.3.15"
description = "Agent Commerce Protocol Python SDK by Virtuals"
authors = ["Steven Lee Soon Fatt <steven@virtuals.io>"]
readme = "README.md"
Expand Down
212 changes: 212 additions & 0 deletions virtuals_acp/abis/single_signer_validation_module_abi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
SINGLE_SIGNER_VALIDATION_MODULE_ABI = [
{ "inputs": [], "name": "InvalidSignatureType", "type": "error" },
{
"inputs": [],
"name": "NotAuthorized",
"type": "error",
},
{ "inputs": [], "name": "NotImplemented", "type": "error" },
{
"inputs": [],
"name": "UnexpectedDataPassed",
"type": "error",
},
{
"anonymous": True,
"inputs": [
{
"indexed": True,
"internalType": "address",
"name": "account",
"type": "address",
},
{
"indexed": True,
"internalType": "uint32",
"name": "entityId",
"type": "uint32",
},
{
"indexed": True,
"internalType": "address",
"name": "newSigner",
"type": "address",
},
{
"indexed": False,
"internalType": "address",
"name": "previousSigner",
"type": "address",
},
],
"name": "SignerTransferred",
"type": "event",
},
{
"inputs": [],
"name": "moduleId",
"outputs": [{ "internalType": "string", "name": "", "type": "string" }],
"stateMutability": "pure",
"type": "function",
},
{
"inputs": [{ "internalType": "bytes", "name": "data", "type": "bytes" }],
"name": "onInstall",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [{ "internalType": "bytes", "name": "data", "type": "bytes" }],
"name": "onUninstall",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [
{ "internalType": "address", "name": "account", "type": "address" },
{
"internalType": "bytes32",
"name": "hash",
"type": "bytes32",
},
],
"name": "replaySafeHash",
"outputs": [{ "internalType": "bytes32", "name": "", "type": "bytes32" }],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{ "internalType": "uint32", "name": "entityId", "type": "uint32" },
{
"internalType": "address",
"name": "account",
"type": "address",
},
],
"name": "signers",
"outputs": [{ "internalType": "address", "name": "", "type": "address" }],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [{ "internalType": "bytes4", "name": "interfaceId", "type": "bytes4" }],
"name": "supportsInterface",
"outputs": [{ "internalType": "bool", "name": "", "type": "bool" }],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{ "internalType": "uint32", "name": "entityId", "type": "uint32" },
{
"internalType": "address",
"name": "newSigner",
"type": "address",
},
],
"name": "transferSigner",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function",
},
{
"inputs": [
{ "internalType": "address", "name": "account", "type": "address" },
{
"internalType": "uint32",
"name": "entityId",
"type": "uint32",
},
{ "internalType": "address", "name": "sender", "type": "address" },
{
"internalType": "uint256",
"name": "",
"type": "uint256",
},
{ "internalType": "bytes", "name": "", "type": "bytes" },
{
"internalType": "bytes",
"name": "",
"type": "bytes",
},
],
"name": "validateRuntime",
"outputs": [],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{ "internalType": "address", "name": "account", "type": "address" },
{
"internalType": "uint32",
"name": "entityId",
"type": "uint32",
},
{ "internalType": "address", "name": "", "type": "address" },
{
"internalType": "bytes32",
"name": "digest",
"type": "bytes32",
},
{ "internalType": "bytes", "name": "signature", "type": "bytes" },
],
"name": "validateSignature",
"outputs": [{ "internalType": "bytes4", "name": "", "type": "bytes4" }],
"stateMutability": "view",
"type": "function",
},
{
"inputs": [
{
"internalType": "uint32",
"name": "entityId",
"type": "uint32",
},
{
"components": [
{ "internalType": "address", "name": "sender", "type": "address" },
{
"internalType": "uint256",
"name": "nonce",
"type": "uint256",
},
{ "internalType": "bytes", "name": "initCode", "type": "bytes" },
{
"internalType": "bytes",
"name": "callData",
"type": "bytes",
},
{
"internalType": "bytes32",
"name": "accountGasLimits",
"type": "bytes32",
},
{
"internalType": "uint256",
"name": "preVerificationGas",
"type": "uint256",
},
{ "internalType": "bytes32", "name": "gasFees", "type": "bytes32" },
{
"internalType": "bytes",
"name": "paymasterAndData",
"type": "bytes",
},
{ "internalType": "bytes", "name": "signature", "type": "bytes" },
],
"internalType": "struct PackedUserOperation",
"name": "userOp",
"type": "tuple",
},
{ "internalType": "bytes32", "name": "userOpHash", "type": "bytes32" },
],
"name": "validateUserOp",
"outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }],
"stateMutability": "view",
"type": "function",
},
]
5 changes: 2 additions & 3 deletions virtuals_acp/alchemy.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import secrets
import time
from typing import Dict, Any, List, Optional, Union
from dataclasses import dataclass
from enum import Enum
from typing import Dict, Any, List, Optional, Union

import requests
from eth_account import Account
from eth_account.messages import encode_defunct
from eth_account.messages import encode_typed_data
from eth_utils.conversions import to_hex
from eth_account.messages import encode_defunct


from virtuals_acp.configs.configs import BASE_SEPOLIA_CONFIG, ACPContractConfig
from virtuals_acp.models import OperationPayload
Expand Down
4 changes: 3 additions & 1 deletion virtuals_acp/configs/configs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# virtuals_acp/configs.py
from typing import Literal, Optional, Union, List, Dict, Any
from web3 import Web3

from typing import Literal, Optional, List, Dict, Any
from virtuals_acp.fare import Fare
from virtuals_acp.abis.abi import ACP_ABI
from virtuals_acp.abis.abi_v2 import ACP_V2_ABI
Expand Down
6 changes: 4 additions & 2 deletions virtuals_acp/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from web3 import Web3

USDC_TOKEN_ADDRESS = {
84532: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
8453: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
Expand All @@ -13,6 +15,6 @@
{"name": "nonce", "type": "bytes32"},
]

VERIFYING_CONTRACT_ADDRESS = "0x00000000000099DE0BF6fA90dEB851E2A2df7d83"

HTTP_STATUS_CODES_X402 = {"Payment Required": 402, "OK": 200}

SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS = Web3.to_checksum_address("0x00000000000099DE0BF6fA90dEB851E2A2df7d83")
59 changes: 55 additions & 4 deletions virtuals_acp/contract_clients/base_contract_client.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import json
from abc import ABC, abstractmethod
from datetime import datetime
from decimal import Decimal
import math
from typing import Dict, Any, Optional, List, cast

from eth_typing import ABIEvent
import requests
from ens.utils import is_none_or_zero_address
from web3 import Web3
from web3.contract import Contract
from eth_utils.abi import event_abi_to_log_topic

from virtuals_acp.abis.erc20_abi import ERC20_ABI
from virtuals_acp.abis.flat_token_v2_abi import FIAT_TOKEN_V2_ABI
from virtuals_acp.abis.single_signer_validation_module_abi import SINGLE_SIGNER_VALIDATION_MODULE_ABI
from virtuals_acp.abis.weth_abi import WETH_ABI
from virtuals_acp.constants import SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS
from virtuals_acp.fare import WETH_FARE
from virtuals_acp.configs.configs import ACPContractConfig
from virtuals_acp.exceptions import ACPError
Expand All @@ -26,7 +29,6 @@
X402PayableRequirements,
OperationPayload,
OffChainJob,
X402PaymentResponse,
)


Expand Down Expand Up @@ -67,9 +69,58 @@ def __init__(self, agent_wallet_address: str, config: ACPContractConfig):
self.job_created_event_signature_hex = (
"0x" + event_abi_to_log_topic(cast(ABIEvent, job_created_event_abi)).hex()
)


agent_smart_contract_code = self.w3.eth.get_code(Web3.to_checksum_address(self.agent_wallet_address))
is_account_deployed = len(agent_smart_contract_code) > 0
if not is_account_deployed:
raise ACPError(f"ACP Contract Client validation failed: agent account {self.agent_wallet_address} is not deployed on-chain")

def validate_session_key_on_chain(
self,
session_signer_address: str,
session_entity_key_id: int
) -> None:
single_signer_validation_contract: Contract = self.w3.eth.contract(
address=SINGLE_SIGNER_VALIDATION_MODULE_ADDRESS,
abi=SINGLE_SIGNER_VALIDATION_MODULE_ABI,
)
on_chain_signer_address = (
single_signer_validation_contract
.functions
.signers(session_entity_key_id, self.agent_wallet_address)
.call()
)

if is_none_or_zero_address(on_chain_signer_address):
raise ACPError(
"ACP Contract Client validation failed:\n" +
json.dumps(
{
"reason": "no whitelisted wallet registered on-chain for entity id",
"entity_id": session_entity_key_id,
"agent_wallet_address": self.agent_wallet_address,
},
indent=2
)
)

if on_chain_signer_address.lower() != session_signer_address.lower():
raise ACPError(
"ACP Contract Client validation failed:\n" +
json.dumps(
{
Comment thread
Ang-dot marked this conversation as resolved.
"agent_wallet_address": self.agent_wallet_address,
"entity_id": session_entity_key_id,
"given_whitelisted_wallet_address": session_signer_address,
"expected_whitelisted_wallet_address": on_chain_signer_address,
"reason": "session signer address mismatch",
},
indent=2
)
)

@abstractmethod
def getAcpVersion(self) -> str:
def get_acp_version(self) -> str:
pass

def _build_user_operation(
Expand Down
Loading