Skip to content

feat(wallet): Auto-create Coinbase wallet if not found#2120

Open
phuongdao03 wants to merge 2 commits intoOpenMind:mainfrom
phuongdao03:feat-create-wallet-if-not-found
Open

feat(wallet): Auto-create Coinbase wallet if not found#2120
phuongdao03 wants to merge 2 commits intoOpenMind:mainfrom
phuongdao03:feat-create-wallet-if-not-found

Conversation

@phuongdao03
Copy link

Problem

Resolves TODO(Kyle) in src/inputs/plugins/wallet_coinbase.py:

TODO(Kyle): Create Wallet if the wallet ID is not found

Currently, users must manually create a Coinbase wallet and set COINBASE_WALLET_ID environment variable before using the wallet integration. This creates friction for new users.

Solution

Implement automatic wallet creation with persistence:

  1. Priority-based wallet retrieval:

    • First try COINBASE_WALLET_ID environment variable
    • Then try loading from saved seed file
    • Finally, create new wallet and save seed
  2. Wallet persistence:

    • Save wallet seed to ~/.om1/coinbase_wallet_seed.json
    • Secure file permissions (0600)
    • Enables signing capabilities on subsequent runs
  3. New configuration options:

    • network_id: Configure target network (default: base-sepolia)
    • wallet_seed_file: Custom path for seed storage

Testing

  • Verified syntax with python -m py_compile
  • Manual testing with CDP SDK

Security Considerations

  • Wallet seed file has restrictive permissions (owner-only)
  • Seed is required for signing transactions

Implements automatic wallet creation when COINBASE_WALLET_ID is not set.
This resolves the TODO(Kyle) comment in the codebase.

Changes:
- Add _get_or_create_wallet() method with priority-based wallet retrieval:
  1. Try COINBASE_WALLET_ID environment variable first
  2. Load from saved seed file if exists
  3. Create new wallet and persist seed for future use

- Add wallet persistence via seed file (~/.om1/coinbase_wallet_seed.json)
  - Saves wallet_id, seed, and network_id
  - Sets restrictive permissions (0600) for security

- Add new config options:
  - network_id: Configure network (base-sepolia, base-mainnet, etc.)
  - wallet_seed_file: Custom path for wallet seed storage

- Improve error handling and logging throughout

This enables users to start using Coinbase wallet integration without
manually creating a wallet first. The wallet is automatically persisted
so subsequent runs use the same wallet.
@phuongdao03 phuongdao03 requested a review from a team as a code owner February 5, 2026 15:41
Copilot AI review requested due to automatic review settings February 5, 2026 15:41
@phuongdao03 phuongdao03 requested a review from a team as a code owner February 5, 2026 15:41
@github-actions github-actions bot added robotics Robotics code changes python Python code labels Feb 5, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements automatic wallet creation for the Coinbase wallet integration, resolving a TODO in the codebase. Previously, users had to manually create a wallet and set the COINBASE_WALLET_ID environment variable before using the integration.

Changes:

  • Implements priority-based wallet retrieval: environment variable → saved seed file → new wallet creation
  • Adds wallet seed persistence to ~/.om1/coinbase_wallet_seed.json with restrictive file permissions (0o600)
  • Adds new configuration options: network_id and wallet_seed_file for customizing wallet creation and seed storage

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 173 to 185
wallet = Wallet.fetch(wallet_id)

# Load seed for signing capabilities
if seed:
wallet.load_seed(self.wallet_seed_file)
logging.info(f"Loaded wallet with signing capabilities: {wallet.id}")
else:
logging.info(f"Loaded wallet (view-only): {wallet.id}")

return wallet
except Exception as e:
logging.warning(f"Could not load wallet from seed file: {e}")
return None
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

When loading from a seed file, if the wallet no longer exists on the Coinbase server (e.g., it was deleted), the Wallet.fetch(wallet_id) call will fail and the method returns None. This causes the system to create a new wallet in _create_new_wallet(), losing the original wallet's seed data. Consider using a method like Wallet.import_data() to restore the wallet from the seed data, or at minimum preserve the seed file when creation of a new wallet occurs to avoid data loss.

Copilot uses AI. Check for mistakes.
asset_id: str = Field(default="eth", description="Asset ID to query")
network_id: str = Field(
default="base-sepolia",
description="Network ID for wallet creation (e.g., base-sepolia, base-mainnet)",
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The documentation for network_id field mentions example values like "base-sepolia" and "base-mainnet", but doesn't provide guidance on what other valid values exist or how users can discover them. Consider adding a reference to the CDP SDK documentation or listing all supported network IDs to help users configure this correctly.

Suggested change
description="Network ID for wallet creation (e.g., base-sepolia, base-mainnet)",
description=(
"Network ID for wallet creation (e.g., base-sepolia, base-mainnet). "
"Refer to the CDP SDK documentation for the full list of supported network IDs."
),

Copilot uses AI. Check for mistakes.
Comment on lines 85 to +92
if not API_KEY or not API_SECRET:
logging.error(
"COINBASE_API_KEY or COINBASE_API_SECRET environment variable is not set"
)
else:
Cdp.configure(API_KEY, API_SECRET)

try:
# fetch wallet data
if not self.COINBASE_WALLET_ID:
raise ValueError("COINBASE_WALLET_ID environment variable is not set")
self.wallet = None
self.balance = 0.0
self.balance_previous = 0.0
return
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The early return when API keys are missing will break the existing test test_initialization_without_api_keys_does_not_call_configure (tests/inputs/plugins/test_wallet_coinbase.py:98-124). The test expects the wallet to still be initialized when API keys are missing, but this code now returns early with wallet set to None. This changes the behavior from the original implementation.

Copilot uses AI. Check for mistakes.
Comment on lines 114 to 269
def _get_or_create_wallet(self) -> Optional[Wallet]:
"""
Get existing wallet or create a new one if not found.

Priority:
1. Use COINBASE_WALLET_ID environment variable if set
2. Load wallet from saved seed file if exists
3. Create new wallet and save seed for future use

Returns
-------
Optional[Wallet]
The wallet instance, or None if creation/fetch failed.
"""
# Priority 1: Try to fetch wallet using environment variable
if self.COINBASE_WALLET_ID:
try:
wallet = Wallet.fetch(self.COINBASE_WALLET_ID)
logging.info(f"Fetched existing wallet: {wallet.id}")

# Try to load seed if available for signing capabilities
self._load_wallet_seed(wallet)
return wallet
except Exception as e:
logging.warning(
f"Could not fetch wallet with ID {self.COINBASE_WALLET_ID}: {e}"
)

# Priority 2: Try to load wallet from saved seed file
wallet = self._load_wallet_from_seed_file()
if wallet:
return wallet

# Priority 3: Create new wallet
return self._create_new_wallet()

def _load_wallet_from_seed_file(self) -> Optional[Wallet]:
"""
Load wallet from saved seed file.

Returns
-------
Optional[Wallet]
The wallet instance if loaded successfully, None otherwise.
"""
try:
if not os.path.exists(self.wallet_seed_file):
return None

with open(self.wallet_seed_file, "r") as f:
wallet_data = json.load(f)

wallet_id = wallet_data.get("wallet_id")
seed = wallet_data.get("seed")

if not wallet_id:
logging.warning("Wallet seed file exists but missing wallet_id")
return None

wallet = Wallet.fetch(wallet_id)

# Load seed for signing capabilities
if seed:
wallet.load_seed(self.wallet_seed_file)
logging.info(f"Loaded wallet with signing capabilities: {wallet.id}")
else:
logging.info(f"Loaded wallet (view-only): {wallet.id}")

return wallet
except Exception as e:
logging.warning(f"Could not load wallet from seed file: {e}")
return None

def _load_wallet_seed(self, wallet: Wallet) -> None:
"""
Attempt to load seed for an existing wallet to enable signing.

Parameters
----------
wallet : Wallet
The wallet to load seed for.
"""
try:
if os.path.exists(self.wallet_seed_file):
with open(self.wallet_seed_file, "r") as f:
wallet_data = json.load(f)

if wallet_data.get("wallet_id") == wallet.id and wallet_data.get(
"seed"
):
wallet.load_seed(self.wallet_seed_file)
logging.info(f"Loaded seed for wallet: {wallet.id}")
except Exception as e:
logging.debug(f"Could not load seed for wallet: {e}")

def _create_new_wallet(self) -> Optional[Wallet]:
"""
Create a new wallet and save its seed for future use.

Returns
-------
Optional[Wallet]
The newly created wallet, or None if creation failed.
"""
try:
logging.info(f"Creating new wallet on network: {self.network_id}")
wallet = Wallet.create(network_id=self.network_id)

# Save wallet data for future use
self._save_wallet_seed(wallet)

logging.info(f"Created new wallet: {wallet.id}")
logging.info(
f"Default address: {wallet.default_address.address_id if wallet.default_address else 'N/A'}"
)

# Set environment variable for current session
os.environ["COINBASE_WALLET_ID"] = wallet.id

return wallet
except Exception as e:
logging.error(f"Failed to create new wallet: {e}")
return None

def _save_wallet_seed(self, wallet: Wallet) -> None:
"""
Save wallet seed to file for future use.

Parameters
----------
wallet : Wallet
The wallet to save.
"""
try:
# Ensure directory exists
seed_dir = os.path.dirname(self.wallet_seed_file)
if seed_dir:
Path(seed_dir).mkdir(parents=True, exist_ok=True)

# Export and save wallet data
wallet_data = wallet.export_data()
save_data = {
"wallet_id": wallet_data.wallet_id,
"seed": wallet_data.seed,
"network_id": wallet_data.network_id,
}

with open(self.wallet_seed_file, "w") as f:
json.dump(save_data, f, indent=2)

# Set restrictive permissions (owner read/write only)
os.chmod(self.wallet_seed_file, 0o600)

logging.info(f"Wallet seed saved to: {self.wallet_seed_file}")
except Exception as e:
logging.error(f"Failed to save wallet seed: {e}")
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The new wallet creation and seed file persistence functionality lacks test coverage. The following scenarios should be tested:

  1. Creating a new wallet when no wallet ID or seed file exists
  2. Loading a wallet from a seed file when COINBASE_WALLET_ID is not set
  3. Saving wallet seed data with correct permissions (0o600)
  4. Handling cases where seed file exists but is malformed
  5. Fallback behavior when wallet creation fails
  6. The priority order: env var -> seed file -> create new

Since other plugins in tests/inputs/plugins/ have comprehensive test coverage, this new functionality should follow the same pattern.

Copilot uses AI. Check for mistakes.

# Load seed for signing capabilities
if seed:
wallet.load_seed(self.wallet_seed_file)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The usage of wallet.load_seed(self.wallet_seed_file) appears incorrect. According to the CDP SDK patterns, load_seed likely expects the actual seed data or a wallet data object, not a file path. The code already reads the JSON file and extracts wallet_data, but then passes the file path string instead of using the extracted seed data. Consider using the Wallet.import_data() method or passing the actual seed/wallet_data object to properly restore signing capabilities.

Copilot uses AI. Check for mistakes.
if wallet_data.get("wallet_id") == wallet.id and wallet_data.get(
"seed"
):
wallet.load_seed(self.wallet_seed_file)
Copy link

Copilot AI Feb 5, 2026

Choose a reason for hiding this comment

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

The usage of wallet.load_seed(self.wallet_seed_file) appears incorrect. According to the CDP SDK patterns, load_seed likely expects the actual seed data or a wallet data object, not a file path. The code already reads the JSON file and extracts wallet_data, but then passes the file path string instead of using the extracted seed data. Consider using the Wallet.import_data() method or passing the actual seed/wallet_data object to properly restore signing capabilities.

Copilot uses AI. Check for mistakes.
Addresses all review comments from GitHub Copilot:

1. Use Wallet.import_data instead of deprecated load_seed
   - Replaced load_seed() with Wallet.import_data(WalletData(...))
   - Uses WalletData class for proper wallet restoration

2. Handle wallet deleted from server
   - Added _recreate_wallet_from_seed() method
   - When import_data fails, attempts to recreate wallet with same seed
   - Preserves wallet addresses derived from original seed

3. Fix seed_dir edge case
   - Added comment explaining os.path.dirname returns empty string
     for files in current directory
   - Current directory always exists, so this is acceptable

4. Improve error handling when seed save fails
   - _save_wallet_seed now returns bool to indicate success
   - Logs CRITICAL warning with wallet ID when save fails
   - Allows users to manually save wallet ID

5. Add comprehensive documentation for network_id
   - Lists supported networks (base-sepolia, base-mainnet, etc.)
   - Includes link to CDP SDK documentation

6. Add unit tests for new functionality
   - Test wallet creation when no ID set
   - Test loading from seed file
   - Test malformed seed file handling
   - Test wallet recreation when deleted from server
   - Test priority order (env var > seed file > create new)
   - Test custom network_id configuration
@phuongdao03 phuongdao03 requested a review from a team as a code owner February 5, 2026 16:03
@github-actions github-actions bot added the tests Test files label Feb 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Python code robotics Robotics code changes tests Test files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant