-
Notifications
You must be signed in to change notification settings - Fork 16
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
feat(PSDK-670): Support external wallet imports, wallet imports from CDP Python SDK #68
Changes from all commits
3c1cda5
215b841
c41e702
c970a42
87b8cd8
3a1dee0
c003130
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from dataclasses import dataclass | ||
|
||
|
||
@dataclass | ||
class MnemonicSeedPhrase: | ||
"""Class representing a BIP-39mnemonic seed phrase. | ||
|
||
Used to import external wallets into CDP as 1-of-1 wallets. | ||
|
||
Args: | ||
mnemonic_phrase (str): A valid BIP-39 mnemonic phrase (12, 15, 18, 21, or 24 words). | ||
|
||
""" | ||
|
||
mnemonic_phrase: str |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,7 +9,7 @@ | |
from typing import Any, Union | ||
|
||
import coincurve | ||
from bip_utils import Bip32Slip10Secp256k1 | ||
from bip_utils import Bip32Slip10Secp256k1, Bip39MnemonicValidator, Bip39SeedGenerator | ||
from Crypto.Cipher import AES | ||
from cryptography.hazmat.primitives import serialization | ||
from cryptography.hazmat.primitives.asymmetric import ec | ||
|
@@ -31,6 +31,7 @@ | |
from cdp.faucet_transaction import FaucetTransaction | ||
from cdp.fund_operation import FundOperation | ||
from cdp.fund_quote import FundQuote | ||
from cdp.mnemonic_seed_phrase import MnemonicSeedPhrase | ||
from cdp.payload_signature import PayloadSignature | ||
from cdp.smart_contract import SmartContract | ||
from cdp.trade import Trade | ||
|
@@ -118,7 +119,7 @@ def create( | |
interval_seconds: float = 0.2, | ||
timeout_seconds: float = 20, | ||
) -> "Wallet": | ||
"""Create a new wallet. | ||
"""Create a new wallet with a random seed. | ||
|
||
Args: | ||
network_id (str): The network ID of the wallet. Defaults to "base-sepolia". | ||
|
@@ -131,6 +132,36 @@ def create( | |
Raises: | ||
Exception: If there's an error creating the wallet. | ||
|
||
""" | ||
return cls.create_with_seed( | ||
seed=None, | ||
network_id=network_id, | ||
interval_seconds=interval_seconds, | ||
timeout_seconds=timeout_seconds, | ||
) | ||
|
||
@classmethod | ||
def create_with_seed( | ||
cls, | ||
seed: str | None = None, | ||
network_id: str = "base-sepolia", | ||
interval_seconds: float = 0.2, | ||
timeout_seconds: float = 20, | ||
) -> "Wallet": | ||
"""Create a new wallet with the given seed. | ||
|
||
Args: | ||
seed (str): The seed to use for the wallet. If None, a random seed will be generated. | ||
network_id (str): The network ID of the wallet. Defaults to "base-sepolia". | ||
interval_seconds (float): The interval between checks in seconds. Defaults to 0.2. | ||
timeout_seconds (float): The maximum time to wait for the server signer to be active. Defaults to 20. | ||
|
||
Returns: | ||
Wallet: The created wallet object. | ||
|
||
Raises: | ||
Exception: If there's an error creating the wallet. | ||
|
||
""" | ||
create_wallet_request = CreateWalletRequest( | ||
wallet=CreateWalletRequestWallet( | ||
|
@@ -139,7 +170,7 @@ def create( | |
) | ||
|
||
model = Cdp.api_clients.wallets.create_wallet(create_wallet_request) | ||
wallet = cls(model) | ||
wallet = cls(model, seed) | ||
|
||
if Cdp.use_server_signer: | ||
wallet._wait_for_signer(interval_seconds, timeout_seconds) | ||
|
@@ -228,29 +259,65 @@ def list(cls) -> Iterator["Wallet"]: | |
page = response.next_page | ||
|
||
@classmethod | ||
def import_data(cls, data: WalletData) -> "Wallet": | ||
"""Import a wallet from previously exported wallet data. | ||
def import_wallet(cls, data: WalletData | MnemonicSeedPhrase) -> "Wallet": | ||
"""Import a wallet from previously exported wallet data or a mnemonic seed phrase. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is importing a wallet from previously exported wallet data or "creating a new wallet" from a mnemonic seed phrase. It feels like we should split out the "load" from the "create" flows here? Or is the intent to lazily create the wallet (if one does not exist)? In that world we'd need to be able to check if a wallet already exists when providing the Mnemonic Seed Phrase as well? @derek-cb @John-peterson-coinbase what are y'alls thoughts here? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Edit: See that this is a 2-step plan: Seems reasonable, although I feel like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ah yes good point. while this is a two-step plan, as you mentioned it doesn't quite make sense in the context of the Python SDK since the existing method is called "import_data". i would propose:
@alex-stone what do you think? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That seems reasonable! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @alex-stone committed now |
||
|
||
Args: | ||
data (WalletData): The wallet data to import. | ||
data (Union[WalletData, MnemonicSeedPhrase]): Either: | ||
- WalletData: The wallet data to import, containing wallet_id and seed | ||
- MnemonicSeedPhrase: A valid BIP-39 mnemonic phrase object for importing external wallets | ||
|
||
Returns: | ||
Wallet: The imported wallet. | ||
|
||
Raises: | ||
ValueError: If data is not a WalletData or MnemonicSeedPhrase instance. | ||
ValueError: If the mnemonic phrase is invalid. | ||
Exception: If there's an error getting the wallet. | ||
|
||
""" | ||
if not isinstance(data, WalletData): | ||
raise ValueError("Data must be a WalletData instance") | ||
if isinstance(data, MnemonicSeedPhrase): | ||
# Validate mnemonic phrase | ||
if not data.mnemonic_phrase: | ||
raise ValueError("BIP-39 mnemonic seed phrase must be provided") | ||
|
||
model = Cdp.api_clients.wallets.get_wallet(data.wallet_id) | ||
# Validate the mnemonic using bip_utils | ||
if not Bip39MnemonicValidator().IsValid(data.mnemonic_phrase): | ||
raise ValueError("Invalid BIP-39 mnemonic seed phrase") | ||
|
||
wallet = cls(model, data.seed) | ||
# Convert mnemonic to seed | ||
seed_bytes = Bip39SeedGenerator(data.mnemonic_phrase).Generate() | ||
seed = seed_bytes.hex() | ||
|
||
wallet._set_addresses() | ||
# Create wallet using the provided seed | ||
wallet = cls.create_with_seed(seed=seed) | ||
wallet._set_addresses() | ||
return wallet | ||
|
||
return wallet | ||
elif isinstance(data, WalletData): | ||
model = Cdp.api_clients.wallets.get_wallet(data.wallet_id) | ||
wallet = cls(model, data.seed) | ||
wallet._set_addresses() | ||
return wallet | ||
|
||
raise ValueError("Data must be a WalletData or MnemonicSeedPhrase instance") | ||
|
||
@classmethod | ||
def import_data(cls, data: WalletData) -> "Wallet": | ||
"""Import a wallet from previously exported wallet data. | ||
|
||
Args: | ||
data (WalletData): The wallet data to import. | ||
|
||
Returns: | ||
Wallet: The imported wallet. | ||
|
||
Raises: | ||
ValueError: If data is not a WalletData instance. | ||
Exception: If there's an error getting the wallet. | ||
|
||
""" | ||
return cls.import_wallet(data) | ||
|
||
def create_address(self) -> "WalletAddress": | ||
"""Create a new address for the wallet. | ||
|
@@ -495,6 +562,28 @@ def export_data(self) -> WalletData: | |
return WalletData(self.id, self._seed, self.network_id) | ||
|
||
def save_seed(self, file_path: str, encrypt: bool | None = False) -> None: | ||
"""[Save the wallet seed to a file (deprecated). | ||
|
||
This method is deprecated, and will be removed in a future version. Use load_seed_from_file() instead. | ||
|
||
Args: | ||
file_path (str): The path to the file where the seed will be saved. | ||
encrypt (Optional[bool]): Whether to encrypt the seed before saving. Defaults to False. | ||
|
||
Raises: | ||
ValueError: If the wallet does not have a seed loaded. | ||
|
||
""" | ||
import warnings | ||
|
||
warnings.warn( | ||
"save_seed() is deprecated and will be removed in a future version. Use save_seed_to_file() instead.", | ||
DeprecationWarning, | ||
stacklevel=2, | ||
) | ||
self.save_seed_to_file(file_path, encrypt) | ||
|
||
def save_seed_to_file(self, file_path: str, encrypt: bool | None = False) -> None: | ||
"""Save the wallet seed to a file. | ||
|
||
Args: | ||
|
@@ -537,6 +626,26 @@ def save_seed(self, file_path: str, encrypt: bool | None = False) -> None: | |
json.dump(existing_seeds, f, indent=4) | ||
|
||
def load_seed(self, file_path: str) -> None: | ||
"""Load the wallet seed from a file (deprecated). | ||
|
||
This method is deprecated, and will be removed in a future version. Use load_seed_from_file() instead. | ||
|
||
Args: | ||
file_path (str): The path to the file containing the seed data. | ||
|
||
Raises: | ||
ValueError: If the file does not contain seed data for this wallet or if decryption fails. | ||
|
||
""" | ||
import warnings | ||
warnings.warn( | ||
"load_seed() is deprecated and will be removed in a future version. Use load_seed_from_file() instead.", | ||
DeprecationWarning, | ||
stacklevel=2, | ||
) | ||
self.load_seed_from_file(file_path) | ||
|
||
def load_seed_from_file(self, file_path: str) -> None: | ||
"""Load the wallet seed from a file. | ||
|
||
Args: | ||
|
@@ -685,8 +794,8 @@ def _validate_seed(self, seed: bytes) -> None: | |
ValueError: If the seed length is invalid. | ||
|
||
""" | ||
if len(seed) != 64: | ||
raise ValueError("Invalid seed length") | ||
if len(seed) != 32 and len(seed) != 64: | ||
raise ValueError("Seed must be 32 or 64 bytes") | ||
|
||
def _derive_key(self, index: int) -> Bip32Slip10Secp256k1: | ||
"""Derive a key from the master node. | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
lint changes