Skip to content
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

SmartWallet support #105

Open
wants to merge 19 commits into
base: v0.19.0
Choose a base branch
from
Open
24 changes: 24 additions & 0 deletions cdp/call_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from typing import Any, NotRequired, TypedDict, Union

from web3.types import Address, HexStr, Wei


class CallDict(TypedDict):
"""Represents a basic call to a smart contract."""

to: Address
value: NotRequired[Wei]
data: NotRequired[HexStr]


class ABICallDict(TypedDict):
"""Represents a call to a smart contract using ABI encoding."""

to: Address
value: NotRequired[Wei]
abi: list[dict[str, Any]]
function_name: str
args: list[Any]


Call = CallDict | ABICallDict
10 changes: 6 additions & 4 deletions cdp/client/models/create_user_operation_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
import re # noqa: F401
import json

from pydantic import BaseModel, ConfigDict, Field
from typing import Any, ClassVar, Dict, List
from pydantic import BaseModel, ConfigDict, Field, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from cdp.client.models.call import Call
from typing import Optional, Set
from typing_extensions import Self
Expand All @@ -28,7 +28,8 @@ class CreateUserOperationRequest(BaseModel):
CreateUserOperationRequest
""" # noqa: E501
calls: List[Call] = Field(description="The list of calls to make from the smart wallet.")
__properties: ClassVar[List[str]] = ["calls"]
paymaster_url: Optional[StrictStr] = Field(default=None, description="The URL of the paymaster to use for the user operation.")
__properties: ClassVar[List[str]] = ["calls", "paymaster_url"]

model_config = ConfigDict(
populate_by_name=True,
Expand Down Expand Up @@ -88,7 +89,8 @@ def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
return cls.model_validate(obj)

_obj = cls.model_validate({
"calls": [Call.from_dict(_item) for _item in obj["calls"]] if obj.get("calls") is not None else None
"calls": [Call.from_dict(_item) for _item in obj["calls"]] if obj.get("calls") is not None else None,
"paymaster_url": obj.get("paymaster_url")
})
return _obj

Expand Down
69 changes: 69 additions & 0 deletions cdp/network_scoped_smart_wallet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@

from eth_account import Account

from cdp.call_types import Call
from cdp.client.models.smart_wallet import SmartWallet as SmartWalletModel
from cdp.smart_wallet import SmartWallet
from cdp.user_operation import UserOperation


class NetworkScopedSmartWallet(SmartWallet):
"""A smart wallet that's configured for a specific network."""

def __init__(
self,
model: SmartWalletModel,
account: Account,
chain_id: int,
paymaster_url: str | None = None,
) -> None:
"""Initialize the NetworkScopedSmartWallet.

Args:
model (SmartWalletModel): The smart wallet model
account (Account): The account that owns the smart wallet
chain_id (int): The chain ID
paymaster_url (Optional[str]): The paymaster URL

"""
super().__init__(model, account)
self.chain_id = chain_id
self.paymaster_url = paymaster_url

def send_user_operation(
self,
calls: list[Call],
) -> UserOperation:
"""Send a user operation on the configured network.

Args:
calls (List[Call]): The calls to send.

Returns:
UserOperation: The user operation object.

Raises:
ValueError: If there's an error sending the operation.

"""
return super().send_user_operation(
calls=calls, chain_id=self.network.chain_id, paymaster_url=self.network.paymaster_url
)

def __str__(self) -> str:
"""Return a string representation of the NetworkScopedSmartWallet.

Returns:
str: A string representation of the wallet.

"""
return f"Network Scoped Smart Wallet: {self.address} (Chain ID: {self.network.chain_id})"

def __repr__(self) -> str:
"""Return a detailed string representation of the NetworkScopedSmartWallet.

Returns:
str: A detailed string representation of the wallet.

"""
return f"NetworkScopedSmartWallet(model=SmartWalletModel(address='{self.address}'), network=Network(chain_id={self.network.chain_id}, paymaster_url={self.network.paymaster_url!r}))"
130 changes: 130 additions & 0 deletions cdp/smart_wallet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@

from eth_account import Account

from cdp.call_types import Call
from cdp.client.models.smart_wallet import SmartWallet as SmartWalletModel
from cdp.network_scoped_smart_wallet import NetworkScopedSmartWallet
from cdp.user_operation import UserOperation


class SmartWallet:
"""A class representing a smart wallet."""

def __init__(self, model: SmartWalletModel, account: Account) -> None:
"""Initialize the SmartWallet class.

Args:
model (SmartWalletModel): The SmartWalletModel object representing the smart wallet.
account (Account): The owner of the smart wallet.

"""
self._model = model
self.owners = [account]

@property
def address(self) -> str:
"""Get the Smart Wallet Address.

Returns:
str: The Smart Wallet Address.

"""
return self._model.address

@property
def owners(self) -> list[Account]:
"""Get the wallet owners.

Returns:
List[Account]: List of owner accounts

"""
return self.owners

@classmethod
def create(
cls,
account: Account,
) -> "SmartWallet":
"""Create a new smart wallet.

Returns:
Wallet: The created wallet object.

Raises:
Exception: If there's an error creating the wallet.

"""
# TODO: Implement

@classmethod
def to_smart_wallet(cls, smart_wallet_address: str, signer: Account) -> "SmartWallet":
"""Fetch a smart wallet by its ID.

Args:
smart_wallet_address (str): The address of the smart wallet to retrieve.
signer (Account): The signer to use for the smart wallet.

Returns:
SmartWallet: The retrieved smart wallet object.

Raises:
Exception: If there's an error retrieving the smart wallet.

"""
# TODO implement - return object

def use_network(
self, chain_id: int, paymaster_url: str | None = None
) -> NetworkScopedSmartWallet:
"""Configure the wallet for a specific network.

Args:
chain_id (int): The chain ID of the network to connect to
paymaster_url (Optional[str]): Optional URL for the paymaster service

Returns:
NetworkScopedSmartWallet: A network-scoped version of the wallet

"""
return NetworkScopedSmartWallet(self._model, self.owners[0], chain_id, paymaster_url)

def send_user_operation(
self,
calls: list[Call],
chain_id: int,
paymaster_url: str,
) -> UserOperation:
"""Send a user operation.

Args:
calls (List[Call]): The calls to send.
chain_id (int): The chain ID.
paymaster_url (str): The paymaster URL.

Returns:
UserOperation: The user operation object.

Raises:
ValueError: If the default address does not exist.

"""
# TODO implement

def __str__(self) -> str:
"""Return a string representation of the SmartWallet object.

Returns:
str: A string representation of the SmartWallet.

"""
return f"Smart Wallet Address: (id: {self.id})"

def __repr__(self) -> str:
"""Return a string representation of the Wallet object.

Returns:
str: A string representation of the Wallet.

"""
return str(self)
92 changes: 92 additions & 0 deletions cdp/user_operation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@

from cdp.call_types import Call
from cdp.client.models.user_operation import UserOperation as UserOperationModel


class UserOperation:
"""A class representing a user operation."""

def __init__(self, model: UserOperationModel) -> None:
"""Initialize the UserOperation class.

Args:
model (UserOperationModel): The model representing the user operation.

"""
self._model = model

@property
def user_operation_id(self) -> str:
"""Get the user operation ID.

Returns:
str: The user operation ID.

"""
return self._model.user_operation_id

@property
def smart_wallet_address(self) -> str:
"""Get the smart wallet address of the user operation.

Returns:
str: The smart wallet address.

"""
return self._model.smart_wallet_address

@property
def status(self) -> str:
"""Get the status of the contract invocation.

Returns:
str: The status.

"""
return self.transaction.status if self.transaction else None

@classmethod
def create(
cls,
smart_wallet_address: str,
network_id: str,
calls: list[Call],
paymaster_url: str | None = None,
) -> "UserOperation":
"""Create a new UserOperation object.

Args:
smart_wallet_address (str): The smart wallet address.
network_id (str): The Network ID.
calls (list[Call]): The calls to send.
paymaster_url (Optional[str]): The paymaster URL.

Returns:
UserOperation: The new UserOperation object.

"""
# TODO: Implement

def wait(self, interval_seconds: float = 0.2, timeout_seconds: float = 20) -> "UserOperation":
"""Wait until the user operation is processed or fails by polling the server.

Args:
interval_seconds: The interval at which to poll the server.
timeout_seconds: The maximum time to wait before timing out.

Returns:
UserOperation: The completed user operation.

Raises:
TimeoutError: If the user operation takes longer than the given timeout.

"""
# TODO: implement. Note: Will not have a reload function - will simply have the logic here.

def __str__(self) -> str:
"""Return a string representation of the user operation."""
return f"UserOperation: (user_operation_id: {self.user_operation_id}, smart_wallet_address: {self.smart_wallet_address}, status: {self.status})"

def __repr__(self) -> str:
"""Return a string representation of the user operation."""
return str(self)
Loading