Getting Started with Panda

Panda is a blockchain with native Python smart contracts for data science. The fastest way to get started is through the Contract Playground -- a browser-based IDE where you can write, deploy, and interact with contracts in seconds.

Using the Contract Playground

The Playground is available at explorer.pandachain.io/playground. No installation needed.

1. Open the Playground

Navigate to the Contract Playground. You'll see:

  • Editor -- A full-featured code editor with Python syntax highlighting
  • Template Picker -- Pre-built contract templates to start from
  • Deploy Panel -- Connect your wallet and deploy to the chain
  • Interact Panel -- Call and query methods on deployed contracts

2. Choose a Template

Click a template to load it into the editor:

  • Hello World -- Simple greeting contract with state and events
  • PRC-20 Token -- Fungible token with transfers, approvals, and minting
  • PRC-721 NFT -- Non-fungible token collection with ownership and metadata
  • Counter -- Minimal stateful contract
  • Voting -- On-chain governance
  • ML Model -- Train and serve machine learning models on-chain

3. Write Your Contract

All Panda contracts follow the same pattern. Here is the full Counter contract (contracts/examples/counter.py), which demonstrates every core concept:

from panda import contract, constructor, call, query, event


@contract
class Counter:
    """A simple counter that tracks a value and who last modified it."""

    class State:
        count: int = 0
        owner: str = ""
        last_caller: str = ""

    @constructor
    def deploy(self, ctx, initial_count: int = 0):
        """Set the initial count and owner on deployment."""
        self.state.owner = ctx.sender
        self.state.count = initial_count

    @call
    def increment(self, ctx):
        """Add 1 to the counter."""
        self.state.count = self.state.count + 1
        self.state.last_caller = ctx.sender
        self.emit(event.CountChanged(
            new_count=self.state.count,
            changed_by=ctx.sender,
        ))

    @call
    def decrement(self, ctx):
        """Subtract 1 from the counter. Rejects if count would go below 0."""
        if self.state.count <= 0:
            raise ValueError("Counter cannot go below zero")
        self.state.count = self.state.count - 1
        self.state.last_caller = ctx.sender
        self.emit(event.CountChanged(
            new_count=self.state.count,
            changed_by=ctx.sender,
        ))

    @call
    def add(self, ctx, value: int):
        """Add an arbitrary positive value to the counter."""
        if value <= 0:
            raise ValueError("Value must be positive")
        self.state.count = self.state.count + value
        self.state.last_caller = ctx.sender
        self.emit(event.CountChanged(
            new_count=self.state.count,
            changed_by=ctx.sender,
        ))

    @call
    def reset(self, ctx):
        """Reset the counter to 0. Only the owner can reset."""
        if ctx.sender != self.state.owner:
            raise ValueError("Only the owner can reset")
        self.state.count = 0
        self.state.last_caller = ctx.sender
        self.emit(event.CountReset(reset_by=ctx.sender))

    @query
    def get_count(self) -> int:
        """Return the current count."""
        return self.state.count

    @query
    def info(self) -> dict:
        """Return contract metadata."""
        return {
            "count": self.state.count,
            "owner": self.state.owner,
            "last_caller": self.state.last_caller,
        }

Key concepts:

  • @contract -- Marks the class as a deployable contract
  • class State -- Defines on-chain state with typed fields and defaults
  • @constructor -- Runs once at deploy time to initialize state
  • @call -- Methods that modify state (require a transaction and gas)
  • @query -- Read-only methods (free, no transaction)
  • self.emit(event.Name(...)) -- Emits indexable event logs
  • ctx.sender -- The address of the account calling the method

4. Connect Your Wallet

Click Connect Wallet in the Deploy panel. The Playground supports MetaMask and other Web3 wallets.

5. Deploy

Click Deploy to send your contract to the chain. The Playground will:

  1. Encode your Python source as a deploy transaction
  2. Send it to the Panda precompile via your wallet
  3. Show the contract address once mined

6. Interact

Once deployed, the Interact panel shows all @call and @query methods parsed from your contract. Fill in parameters and click to execute.

Using the Panda SDK

For programmatic access, use the Panda SDK:

TypeScript

npm install @panda-sdk/client
import { PandaProvider, Contract } from "@panda-sdk/client";

const provider = new PandaProvider({ rpcUrl: "http://localhost:8545" });
await provider.discoverSender();

// Deploy a contract (using the Counter from contracts/examples/counter.py)
const code = `
from panda import contract, constructor, call, query, event

@contract
class Counter:
    class State:
        count: int = 0
        owner: str = ""
        last_caller: str = ""

    @constructor
    def deploy(self, ctx, initial_count: int = 0):
        self.state.owner = ctx.sender
        self.state.count = initial_count

    @call
    def increment(self, ctx):
        self.state.count = self.state.count + 1
        self.state.last_caller = ctx.sender
        self.emit(event.CountChanged(
            new_count=self.state.count,
            changed_by=ctx.sender,
        ))

    @query
    def get_count(self) -> int:
        return self.state.count
`;

const result = await provider.deploy(code);
console.log("Deployed at:", result.contractAddress);

// Call and query
await provider.call(result.contractAddress, "increment");
const count = await provider.query(result.contractAddress, "get_count");
console.log("Count:", count); // 1

Python

pip install panda-sdk-client
from panda_client import PandaProvider

provider = PandaProvider(rpc_url="http://localhost:8545")
provider.discover_sender()

code = open("counter.py").read()
result = provider.deploy(code)
print(f"Deployed at: {result.contract_address}")

provider.call(result.contract_address, "increment")
count = provider.query(result.contract_address, "get_count")
print(f"Count: {count}")  # 1

Data Science Example

Panda supports on-chain machine learning via panda.ml. Here is the PricePredictor contract (contracts/examples/price_predictor.py), which trains a linear regression model on-chain and serves predictions:

from panda import contract, constructor, call, query, event
from panda.ml import LinearRegression, save_model, load_model, r2_score


@contract
class PricePredictor:
    """Linear regression model for on-chain price prediction."""

    class State:
        owner: str = ""
        asset_name: str = ""
        model: dict = {}
        is_trained: bool = False
        sample_count: int = 0
        last_r2: float = 0.0

    @constructor
    def deploy(self, ctx, asset_name: str = "ETH"):
        self.state.owner = ctx.sender
        self.state.asset_name = asset_name

    @call
    def train(self, ctx, features: list, prices: list):
        if len(features) != len(prices):
            raise ValueError("features and prices must have same length")
        if len(features) < 2:
            raise ValueError("Need at least 2 data points")

        model = LinearRegression()
        model.fit(features, prices)
        preds = model.predict(features)
        r2 = r2_score(prices, preds)

        self.state.model = save_model(model)
        self.state.is_trained = True
        self.state.sample_count = self.state.sample_count + len(features)
        self.state.last_r2 = round(r2, 6)

        self.emit(event.ModelTrained(
            asset=self.state.asset_name,
            samples=len(features),
            r2_score=self.state.last_r2,
            trainer=ctx.sender,
        ))

    @query
    def predict(self, features: list) -> list:
        if not self.state.is_trained:
            raise ValueError("Model not trained yet")
        model = load_model(self.state.model)
        predictions = model.predict(features)
        return [round(p, 2) for p in predictions]

    @query
    def info(self) -> dict:
        return {
            "asset_name": self.state.asset_name,
            "owner": self.state.owner,
            "is_trained": self.state.is_trained,
            "sample_count": self.state.sample_count,
            "last_r2": self.state.last_r2,
        }

The panda.ml module provides deterministic wrappers around scikit-learn models (LinearRegression, LogisticRegression, DecisionTreeClassifier, KMeans, RandomForest, and more). Models are serialized to JSON dicts via save_model() and reconstructed with load_model(), so they can be stored directly in contract state.

Cross-Chain Capabilities

Panda contracts can communicate across chains -- Ethereum, Solana, and between Panda layers. The BridgeV2 contract (contracts/examples/cross_chain_bridge_v2.py) demonstrates async cross-chain calls, fire-and-forget messages, and chain-whitelisted receivers:

from panda import contract, constructor, call, query, receiver, event, Chain


@contract
class BridgeV2:
    class State:
        owner: str = ""
        total_bridged: int = 0
        total_received: int = 0
        pending_bridges: dict = {}
        bridge_count: int = 0

    @constructor
    def deploy(self, ctx):
        self.state.owner = ctx.sender

    @call
    async def bridge_tokens(self, ctx, amount: int, recipient: str = ""):
        """Bridge tokens to Ethereum. Awaits confirmation from Ethereum side."""
        if amount <= 0:
            raise ValueError("Amount must be positive")

        target = recipient or ctx.sender
        eth = Chain.ethereum()

        # Lock tokens on this side
        bridge_id = self.state.bridge_count
        self.state.pending_bridges[str(bridge_id)] = {
            "sender": ctx.sender,
            "amount": amount,
            "recipient": target,
            "status": "pending",
        }
        self.state.bridge_count += 1

        # Send cross-chain call and await confirmation
        _result = await eth.call(
            "0xEthBridge", "lock_and_mint",
            amount=amount, recipient=target,
        )

        # This code runs when the cross-chain response arrives
        self.state.pending_bridges[str(bridge_id)]["status"] = "confirmed"
        self.state.total_bridged += amount
        self.emit(event.BridgeConfirmed(bridge_id=bridge_id, amount=amount))

    @receiver(chains=["ethereum"])
    def on_tokens_locked(self, ctx, amount: int, sender: str):
        """Receive notification that tokens were locked on Ethereum."""
        self.state.total_received += amount
        self.emit(
            event.TokensReceived(
                amount=amount,
                sender=sender,
                source_chain=ctx.source_chain,
                message_id=ctx.message_id,
            )
        )

    @query
    def get_stats(self) -> dict:
        return {
            "total_bridged": self.state.total_bridged,
            "total_received": self.state.total_received,
            "bridge_count": self.state.bridge_count,
        }

Key cross-chain concepts:

  • Chain.ethereum() -- Creates a handle to the Ethereum chain
  • await eth.call(...) -- Async cross-chain call that waits for confirmation
  • @receiver(chains=["ethereum"]) -- Receives inbound messages from whitelisted chains
  • ctx.source_chain, ctx.message_id -- Cross-chain execution context fields

Learn more in the Cross-Chain Messaging guide.

Next Steps