Moccasin

repository·main·Indexed 41 days ago

https://github.com/cyfrin/moccasin

A fast, Pythonic smart contract development and testing framework for Vyper, built on top of Titanoboa. Version 0.4.4 features the mox CLI for compiling, deploying, and initializing projects, as well as a decoupled MetaMask CLI integration for secure transaction signing and account management without direct private key handling.

Tokens
39.3K
Snippets
191
Records
219
Agent score
52%

What's inside Moccasin

  1. Understand the MetaMask CLI Integration module

    main

    The moccasin.metamask_cli_integration module provides a decoupled layer that allows the Moccasin CLI to interact with the MetaMask browser extension. Instead of handling private keys directly in the CLI, this module delegates transaction signing and account management to the MetaMask UI.

    Core Capabilities:

    • Transaction Delegation: Pushes transaction requests to MetaMask for user confirmation and receives the resulting transaction hashes or errors.
    • UI Management: Automatically launches and manages a MetaMask UI tab for network synchronization and account connection.
    • Connection Monitoring: Uses a heartbeat mechanism to detect if the browser tab is closed unexpectedly.
    • Security: Enhances security by avoiding direct private key handling within the CLI environment.
  2. Understand the Moccasin CLI DApp UI Architecture

    main

    The data/metamask_ui directory contains a lightweight, Vanilla JavaScript frontend designed to be served by a Python backend. It facilitates interaction between the Moccasin CLI and the MetaMask browser extension for blockchain operations.

    The architecture is modular to prevent circular dependencies and separate concerns into a clear hierarchy:

    1. View Layer: elements.js (DOM references) and ui.js (visual manipulation).
    2. State Layer: state.js (centralized global application state).
    3. Logic/Service Layer: api.js (Python backend communication), metamask.js (MetaMask/window.ethereum interactions), and polling.js (background interval tasks).
    4. Orchestration Layer: main.js (the entry point that binds the model, view, and services together).
  3. What are Named Contracts and how to use them

    main

    Named contracts in Moccasin allow you to define deployment scripts, contract addresses by chain, and fixture settings for testing within your moccasin.toml configuration. This abstraction allows you to access contracts by a logical name in your Python scripts rather than hardcoding addresses.

    To use a named contract, define it under a specific network in your moccasin.toml and access it via the active_network object in your scripts.

    [networks.mainnet-fork.contracts]
    usdc = { address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"}
    from moccasin.config import get_config
    
    def print_contract_address():
        active_network = get_config().get_active_network()
        named_contract = active_network.get_named_contract("usdc")
        print(named_contract.address)
  4. Use local networks (pyevm and eravm)

    main

    Moccasin provides built-in support for local development networks:

    • pyevm: The basic EVM local network.
    • eravm: The special ZKSync Era local network.

    Note: For these local networks, you cannot manually set url, chain_id, fork, is_zksync, prompt_live, explorer_uri, or explorer_api_key. You can only set the default_account_name.

    [networks.pyevm]
    default_account_name = "anvil"
    
    [networks.eravm]
    default_account_name = "anvil"
  5. Understand the Moccasin project structure

    main

    A standard Moccasin project created via mox init includes the following directory structure:

    • moccasin.toml: The main configuration file for Moccasin.
    • src/: Contains your Vyper smart contracts.
    • script/: Contains Python scripts used for deployment and interaction.
    • tests/: Contains your test files (compatible with pytest).
    • pyproject.toml: (Optional) Used for managing Python dependencies.
    • .vscode/settings.json: (Optional) Contains VSCode-specific settings if --vscode was used.
    • .coveragerc: Configuration for code coverage reporting.
    .
    ├── README.md
    ├── moccasin.toml
    ├── script
    │   ├── __init__.py
    │   └── deploy.py
    ├── src
    │   └── Counter.vy
    └── tests
        ├── conftest.py
        └── test_counter.py
  6. Use `moccasin_main` as a script entry point

    main

    Moccasin recognizes a special function named moccasin_main. If this function exists in your script, Moccasin will execute it by default after running the rest of the script logic. This is useful for organizing your script execution flow.

    Example:

    from src import Counter
    
    def deploy():
        counter = Counter.deploy()
        counter.increment()
        return counter
    
    # Moccasin will automatically call this function
    def moccasin_main():
        deploy()
    from src import Counter
    
    def deploy():
        counter = Counter.deploy()
        counter.increment()
        return counter
    
    def moccasin_main():
        deploy()
  7. Use `get_or_deploy_named_contract` (manifest_named) for portable deployment

    main

    The manifest_named function (also referred to as get_or_deploy_named_contract) allows you to create portable and flexible deployment and scripting pipelines. Instead of writing conditional logic for different networks (e.g., if network == 'mainnet'), you can define contract configurations in moccasin.toml.

    When you call active_network.manifest_named("contract_name"):

    1. It looks up the contract configuration for the current network in moccasin.toml.
    2. If the contract is already deployed on that network (an address is provided), it returns the existing contract instance.
    3. If the contract is not found on that network, it uses the specified deployer_script to deploy it.

    This approach ensures your scripts work identically across local networks, forked networks, testnets, and mainnets.

    from moccasin.config import get_config
    
    def get_decimals():
        active_network = get_config().get_active_network()
        # Returns a VyperContract instance based on moccasin.toml config
        usdc: VyperContract = active_network.manifest_named("usdc")
        decimals = usdc.decimals()
        print(decimals)
  8. Use the pyevm network for local simulation

    main

    If you do not specify a network via command line flags, moccasin defaults to using the pyevm network. This is a built-in network powered by titanoboa that serves as the core engine for moccasin simulations.

    When running scripts on the pyevm network, the network is ephemeral: it exists only for the duration of your script execution. Any state or output you wish to persist must be handled manually within your script.

    mox run deploy
  9. Understand Moccasin configuration precedence

    main

    Moccasin follows a specific hierarchy when resolving configuration values. When a value is requested, Moccasin checks sources in the following order (from highest priority to lowest):

    1. Script: Values explicitly defined within your Python script.
    2. Command Line: Flags and arguments passed via the CLI.
    3. moccasin.toml: Settings defined in the dedicated TOML file.
    4. pyproject.toml: Settings defined under the [tool.moccasin] section.
    5. Defaults: The internal default values set by Moccasin.

    This hierarchy allows you to maintain a base configuration in pyproject.toml or moccasin.toml while overriding specific values on the fly via the CLI or within specific scripts.

  10. Understand Checked vs Unchecked deployment retrieval

    main

    When your smart contract source code changes, the bytecode changes. Moccasin allows you to choose how to handle these changes during retrieval:

    • Unchecked methods (get_deployments_unchecked, get_latest_contract_unchecked, etc.): These return deployments based solely on the contract_name (the filename). They will return all versions of a contract found in the database, even if the bytecode no longer matches your current source file.
    • Checked methods (get_deployments_checked, get_latest_contract_checked, etc.): These perform an integrity check using has_matching_integrity. This compares a hash of the contract bytecode in the database against your current working contract. It will only return deployments that match your current source code.

    Use Checked methods when you want to ensure you are interacting with the exact version of the contract you are currently developing.

  11. Understand the standard Moccasin project layout

    main

    A typical Moccasin project follows a specific directory structure to manage contracts, scripts, and dependencies. While some directories are standard, others are optional or configurable via moccasin.toml.

    Standard directory roles:

    • src/: Contains your Vyper smart contracts.
    • lib/: Contains Moccasin dependencies used inside your contracts.
    • script/: Contains Python scripts used for deployment.
    • tests/: Contains your test suite.
    • out/: (Optional) Contains compiled contracts. Note that in Moccasin and Titanoboa, contracts are often compiled on the fly.
    • moccasin.toml: The central configuration file for managing the project.
    .
    ├── README.md
    ├── lib/
    ├── moccasin.toml
    ├── script/
    ├── src/
    ├── tests/
    └── out/
  12. Key components of MetaMask CLI Integration

    main

    The integration is built around several specialized components that manage the bridge between the CLI and the browser:

    • MetamaskServerControl (server_control.py): Manages the central state, including inter-thread communication queues (CLI ↔ Browser), synchronization events, and server status flags.
    • CustomHandler (http_handler.py): A SimpleHTTPRequestHandler extension that serves static UI files and handles API endpoints like heartbeat, report_transaction_result, and api/network-synced.
    • MetaMaskAccount (account.py): A custom account class implementing send_transaction and sign_typed_data. It communicates with the MetaMask UI to perform signing operations.
    • Lifecycle Functions (server_lifecycle.py):
      • start_metamask_ui_server(): Initializes the server, launches the browser UI, and manages the connection/sync flow.
      • stop_metamask_ui_server(): Gracefully shuts down the server and signals the frontend to disconnect.
    • heartbeat_monitor (utils.py): Monitors the browser tab's activity. It only triggers shutdown warnings if the connection is lost after an account has been successfully connected.