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

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
Draft

SmartWallet support #105

wants to merge 10 commits into from

Conversation

rohan-agarwal-coinbase
Copy link
Contributor

@rohan-agarwal-coinbase rohan-agarwal-coinbase commented Feb 14, 2025

Related PR for Node SDK: coinbase/coinbase-sdk-nodejs#376

Summary

  • Introduces SmartWallet class and NetworkScopedSmartWallet subclass (not using composition here like typescript, inheritance feels more appropriate). I'm using BaseAccount from the eth_accounts library as the owner for the wallet.
  • Introduced EVMCall types for encoded calldata as well as ABI function data. I made this a separate file since we'll use these same types when we improve our existing wallet to support direct calldata and client-side encoding.
  • Introduced UserOperation class which will handle the APIs to interact with creating a user operation, signing it using the base account, and broadcasting it.

Testing

  • E2E testing succeeds
  • TODO unit testing

Notes to reviewers

  • See the test_smart_wallet.py file for what things look like for the DevEx.

Questions

  • Python overall is quite different than how Node SDK PR looks, given we're continuing to wrap underlying models and approach this through object-oriented patterns instead of functional. This feels like the right thing to me to do, but wanted to confirm that we're happy about these patterns deviating across Node and Python.
  • I have a standalone to_smart_wallet function in the SmartWallet class. Based on feedback, I made it standalone instead of a static function since SmartWallet.to_smart_wallet stutters. This naming matches Node SDK - do we want to change any thing about the naming of it though to better suit python best practices?

@cb-heimdall
Copy link

cb-heimdall commented Feb 14, 2025

🟡 Heimdall Review Status

Requirement Status More Info
Reviews 🟡 0/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

@rohan-agarwal-coinbase rohan-agarwal-coinbase changed the title Rohan/cdp 220 SmartWallet support Feb 14, 2025
cdp/call_types.py Outdated Show resolved Hide resolved
cdp/call_types.py Outdated Show resolved Hide resolved
cdp/smart_wallet.py Outdated Show resolved Hide resolved
cdp/smart_wallet.py Outdated Show resolved Hide resolved
cdp/smart_wallet.py Outdated Show resolved Hide resolved
cdp/call_types.py Outdated Show resolved Hide resolved
cdp/network_scoped_smart_wallet.py Show resolved Hide resolved
cdp/network_scoped_smart_wallet.py Show resolved Hide resolved
Comment on lines 47 to 52
self._cdp_client: CdpApiClient = cdp_client
self._wallets: WalletsApi | None = None
self._smart_wallets: SmartWalletsApi | None = None
self._webhooks: WebhooksApi | None = None
self._addresses: AddressesApi | None = None
self._external_addresses: ExternalAddressesApi | None = None

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

general python nit, if you're doing a getter pattern using .__variable = is preferable if possible as it makes the variables private, seeing .smart_wallet and ._smart_wallet can be confusing

@@ -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")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pythonic nit, can be

[Call.from_dict(_item) for _item in obj["calls"]] if "calls" in obj else None,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also don't need the _ prefix

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah this is auto-generated openapi code. we can see if this gets cleaned up in newer versions though, or try to configure the rules to generate these files differently

value: Wei | None = Field(None, description="Amount of native currency to send")
abi: list[dict[str, Any]] = Field(..., description="Contract ABI specification")
function_name: str = Field(..., description="Name of the function to call")
args: list[Any] = Field(..., description="Arguments to pass to the function")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

very very optional nit, but I think you could type args here with list[int | str | ...etc]

# Only SmartWallet related chains are listed here right now
SupportedChainId = Literal[8453, 84532]

_CHAIN_ID_TO_NETWORK_ID: dict[SupportedChainId, NetworkIdentifier] = {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imo this should either be no _ or fully private __

Comment on lines +19 to +20
_chain_id: SupportedChainId
_network_id: NetworkIdentifier

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why _ prefixes? I guess we aren't having users construct these but still is odd to me

Comment on lines +11 to +18
def __init__(
self,
smart_wallet_address: str,
account: BaseAccount,
chain_id: int,
paymaster_url: str | None = None,
) -> None:
"""Initialize the NetworkScopedSmartWallet.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do users construct this? Do they construct this?

Comment on lines +28 to +29
self._smart_wallet_address = smart_wallet_address
self._owners = [account]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same nits as above on _ vs __

@classmethod
def create(
cls,
account: BaseAccount,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still not in love with BaseAccount given when the overloaded meaning

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah yeah this is coming from from eth_account.signers.base import BaseAccount now, not something we've invented.

I can rename it at the import layer but fundamentally this is a 3rd party class named with Base as the prefix

NetworkScopedSmartWallet: A network-scoped version of the wallet

"""
from cdp.network_scoped_smart_wallet import NetworkScopedSmartWallet

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally don't love local imports unless good reason like import cycle (if import cycle that might be a smell our package structure is a little off)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah there's an import cycle here. how do you handle this situation where

class A subclasses class B, and there's a method in class B to construct an instance of class A? that's what is causing the import cycle

)


def test_smart_wallet_operations():

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test doesn't seem to actually fail in a way pytest recognizes, should add asserts

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah this is just a test script, not actually unit tests. this won't get checked in - just for reference

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants