Contract Development Guide

This guide covers everything you need to know about writing, testing, and deploying Python smart contracts on Panda.

Token standards (PRC)

Panda defines chain-level interfaces aligned with common Ethereum EIPs:

StandardSummaryReference
PRC-20Fungible tokens (ERC-20 analogue)docs/PRC20.md, contracts/tokens/prc20_token.py
PRC-721Non-fungible tokens (ERC-721 analogue)docs/PRC721.md, contracts/tokens/prc721_collection.py

Example Contract Library

Panda ships with a comprehensive library of example contracts organized by category:

contracts/tokens/ — Token Standards & DeFi

ContractDescription
prc20_token.pyPRC-20 fungible token (reference implementation)
prc721_collection.pyPRC-721 NFT collection (reference implementation)
iprc20.pyAbstract PRC-20 interface for type safety
iprc721.pyAbstract PRC-721 interface for type safety
prc20_interface.pyPRC-20 implemented via the IPRC20 interface pattern
prc721_interface.pyPRC-721 implemented via the IPRC721 interface pattern
token_swap.pyAMM-style token swap (cross-contract calls to PRC-20s)
nft_marketplace.pyNFT marketplace (cross-contract calls to PRC-721 + PRC-20)

contracts/crypto/ — Cryptographic Primitives

ContractDescription
hash_registry.pyCommit-reveal scheme using SHA-256
merkle_airdrop.pyGas-efficient airdrop with Merkle proof verification
multisig_wallet.pyM-of-N multisig with HMAC-based signer verification
secret_auction.pySealed-bid auction using Pedersen commitments
timelock_vault.pyTime-locked vault with derived-key withdrawal

contracts/patterns/ — Design Patterns & Inheritance

ContractDescription
ownable.pyOwnership pattern (two-step transfer)
pausable.pyPausable extension (inherits Ownable pattern)
access_control.pyRole-based access control (RBAC) with admin hierarchy
registry.pyOn-chain contract discovery and metadata registry
escrow.pyEscrow service using cross-contract PRC-20 transfers
dao_voting.pyDAO governance with cross-contract token balance queries

contracts/ml/ — Machine Learning

12 ML contracts covering regression, classification, clustering, neural networks, ensemble methods, online learning, and model marketplaces.

contracts/privacy/ — Privacy & Federated Learning

Private model inference and federated training contracts.

Contract Types

Panda supports two types of programs:

Stateless Scripts

The simplest programs are plain Python scripts. They take optional input via stdin, execute, and produce output via stdout. No decorators, no SDK import, no state persistence.

# fibonacci.py
def fib(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

print(fib(100))
# analysis.py
import numpy as np
import pandas as pd

data = pd.DataFrame({
    "x": np.random.default_rng(42).normal(0, 1, 1000),
    "y": np.random.default_rng(42).normal(5, 2, 1000),
})
print(data.describe().to_json())

Run stateless scripts with:

panda run fibonacci.py              # Execute locally
panda run fibonacci.py --on-chain   # Execute as on-chain transaction

Stateful Contracts

Stateful contracts use the @contract decorator and maintain persistent on-chain state. They are deployed to an address and called repeatedly via transactions.

Here is the Counter contract (contracts/examples/counter.py), the simplest complete example:

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 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,
        }

HelloWorld

The simplest possible Panda contract. It stores a greeting message, lets anyone update it, and emits an event on each update.

<!-- Source: contracts/examples/hello_world.py -->
"""
HelloWorld -- A simple greeting contract.
Example Panda smart contract demonstrating @contract, @call, @query, @event decorators.
"""

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


@contract
class HelloWorld:
    """A simple greeting contract that stores and updates a message."""

    class State:
        message: str = "Hello, World!"
        owner: str = ""
        greeting_count: int = 0

    @constructor
    def deploy(self, ctx):
        """Called once on deployment."""
        self.state.owner = ctx.sender
        print(f"HelloWorld deployed by {ctx.sender}")

    @call
    def set_message(self, ctx, message: str):
        """Update the greeting message. Increments greeting_count and emits event."""
        self.state.message = message
        self.state.greeting_count = self.state.greeting_count + 1

        self.emit(
            event.MessageUpdated(
                new_message=message,
                greeting_count=self.state.greeting_count,
                updated_by=ctx.sender,
            )
        )
        print(
            f"Message updated to '{message}' (greeting #{self.state.greeting_count}) by {ctx.sender}"
        )

    @query
    def get_message(self) -> str:
        """Return the current greeting message. Read-only operation."""
        return self.state.message

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

This contract demonstrates every core concept in under 50 lines:

  • @contract marks the class as a smart contract
  • class State defines persistent on-chain storage with typed, defaulted fields
  • @constructor runs once at deploy time to set the owner
  • @call methods mutate state and cost gas (set_message updates the message and emits an event)
  • @query methods are read-only and free (get_message returns the current greeting)
  • self.emit(event.Name(...)) records an indexed event in the transaction receipt

Decorators Reference

@contract

Marks a class as a Panda smart contract. Every contract must have an inner State class. Here is the state layout from the PRC20Token contract (contracts/tokens/prc20_token.py):

from panda import contract, constructor, call, query, event
from panda.token import FungibleToken, TokenError


@contract
class PRC20Token:
    class State:
        token: dict = {}
        mint_authority: str = ""
        freeze_authority: str = ""
        frozen: dict = {}

Requirements:

  • The class must contain a State inner class
  • State fields must have type annotations and default values
  • Only one @contract class per file

@call

Marks a method as state-mutating. Calling a @call method produces a transaction, costs gas, and can modify the contract's state.

Here is the transfer method from PRC20Token (contracts/tokens/prc20_token.py):

    @call
    def transfer(self, ctx, to: str, value: int):
        self._not_frozen(ctx.sender)
        self._not_frozen(to)
        t = self._t()
        if value == 0:
            self.state.token = t.to_dict()
            self.emit(event.Transfer(_from=ctx.sender, _to=to, _value=0))
            return
        t.transfer(ctx.sender, to, value)
        self.state.token = t.to_dict()
        self.emit(event.Transfer(_from=ctx.sender, _to=to, _value=value))

The first parameter after self is always ctx (the execution context). Remaining parameters are the method arguments provided by the caller.

@query

Marks a method as read-only. Calling a @query method does not produce a transaction and costs no gas. The method cannot modify state.

Here is the balance_of query from PRC20Token (contracts/tokens/prc20_token.py):

    @query
    def balance_of(self, owner: str) -> int:
        return self._t().balance_of(owner)

Query methods do not receive a ctx parameter. They can only read from self.state, not write to it.

@event

Events are not used as a direct decorator on methods. Instead, events are emitted inside @call methods using self.emit().

Here is an example from the Counter contract (contracts/examples/counter.py):

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

Events appear in transaction receipts and are indexed by the block explorer. The event object is imported from the panda module:

from panda import event

Event names are created dynamically -- you do not need to pre-define them. event.AnyName(key=value) creates an event with that name and the provided fields.

@private

Marks a contract or method as privacy-enabled. Private contracts can have their code and/or state encrypted while remaining verifiable via ZK proofs.

from panda import contract, call, query, private

@contract
@private
class SecretModel:
    class State:
        weights: list[float] = []

    @call
    def update_weights(self, ctx, new_weights: list[float]):
        self.state.weights = new_weights

    @query
    def predict(self, features: list[float]) -> float:
        import numpy as np
        return float(np.dot(self.state.weights, features))

@proof

Specifies the ZK proof type for a method. Two modes are available:

from panda import proof

@call
@proof(type="validity")
def critical_operation(self, ctx, data: list[float]):
    # A ZK validity proof is generated for this execution
    # Proves the computation was correct without re-execution
    pass

@call
@proof(type="fraud")
def optimistic_operation(self, ctx, data: list[float]):
    # Executes optimistically; anyone can submit a fraud proof
    # during the challenge window if the result is incorrect
    pass

Execution Context

The ctx object passed to @call methods provides information about the current execution environment:

@call
def my_method(self, ctx):
    ctx.sender            # Address of the caller (str)
    ctx.block_height      # Current block number (int)
    ctx.block_time        # Block timestamp in seconds (int)
    ctx.contract_address  # This contract's address (str)
    ctx.gas_remaining     # Remaining gas budget (int)
    ctx.chain_id          # "panda-eth-mainnet" or "panda-sol-mainnet" (str)

Use ctx.block_time instead of time.time() or datetime.now() for timestamps. These standard library functions are blocked because they would break determinism.

State Management

Defining State

The State inner class defines the contract's persistent storage. Every field must have a type annotation and a default value:

class State:
    count: int = 0
    name: str = ""
    scores: list[float] = []
    metadata: dict = {}
    is_active: bool = True
    weights: bytes = b""

Supported State Types

TypeDescriptionExample
intInteger (arbitrary precision)count: int = 0
floatIEEE 754 doublescore: float = 0.0
strUTF-8 stringname: str = ""
boolBooleanactive: bool = True
bytesBinary datadata: bytes = b""
listOrdered listitems: list = []
dictKey-value mapbalances: dict = {}
list[float]Typed listweights: list[float] = []

Reading and Writing State

Inside contract methods, access state through self.state. Here is how the PredictionMarket contract (contracts/examples/prediction_market.py) reads and writes state in its place_bet method:

    @call
    def place_bet(self, ctx, side: str, amount: int):
        if self.state.resolved:
            raise ValueError("Market already resolved")
        if ctx.block_time >= self.state.deadline:
            raise ValueError("Betting period has ended")
        if side not in ("yes", "no"):
            raise ValueError("Side must be 'yes' or 'no'")
        if amount <= 0:
            raise ValueError("Amount must be positive")

        addr = ctx.sender
        bets = dict(self.state.bets)
        if addr in bets:
            raise ValueError("Already placed a bet")

        bets[addr] = {"side": side, "amount": amount}
        self.state.bets = bets

        if side == "yes":
            self.state.yes_pool = self.state.yes_pool + amount
        else:
            self.state.no_pool = self.state.no_pool + amount

        self.emit(event.BetPlaced(
            bettor=addr,
            side=side,
            amount=amount,
            yes_pool=self.state.yes_pool,
            no_pool=self.state.no_pool,
        ))

State reads and writes are tracked by PandaVM. Only changed fields are included in the state diff that gets applied to the chain.

State Serialization

State is serialized using MessagePack with deterministic key ordering. This is handled automatically -- you do not need to manage serialization.

Large State (ML Models)

For ML contracts, use panda.ml's save_model() and load_model() to store trained models as JSON dicts in contract state. Here is how the PricePredictor contract (contracts/examples/price_predictor.py) handles model persistence:

from panda.ml import LinearRegression, save_model, load_model, r2_score

# In the train method:
        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

# In the predict query:
        model = load_model(self.state.model)
        predictions = model.predict(features)
        return [round(p, 2) for p in predictions]

Supported Libraries

Tier 1 (First-class, determinism verified)

import numpy as np           # Numerical computation
import pandas as pd          # Data manipulation
import sklearn               # Classical ML (LinearRegression, RandomForest, etc.)
import tensorflow as tf      # Deep learning (CPU only)
import torch                 # PyTorch (CPU only)
import keras                 # High-level neural networks

Tier 2 (Supported, determinism verified)

import scipy                 # Scientific computing
import xgboost               # Gradient boosting
import lightgbm              # Gradient boosting
import statsmodels           # Statistical models

Tier 3 (Available, user-verified)

import polars                # Fast dataframes
import networkx              # Graph algorithms
import sympy                 # Symbolic math
from PIL import Image        # Image processing

Standard Library (Included)

The following standard library modules are available: json, math, decimal, fractions, statistics, collections, itertools, functools, operator, copy, re, struct, hashlib, hmac, base64, binascii, pickle, csv, dataclasses, enum, abc, typing, io, textwrap, string, pprint, bisect, heapq, array, numbers, warnings, contextlib, inspect.

Forbidden Patterns

The contract linter rejects the following at deployment time:

Forbidden Imports

import os                    # No system access
import subprocess            # No process spawning
import socket                # No network access
import threading             # No parallelism
import multiprocessing       # No parallelism
import asyncio               # No async (non-deterministic scheduling)
import ctypes                # No native code loading
import secrets               # Non-deterministic
import uuid                  # Non-deterministic
import requests              # No network
import urllib                # No network
import http.client           # No network
import signal                # No signal handling

Forbidden Patterns

time.time()                  # Use ctx.block_time
datetime.now()               # Use ctx.block_time
datetime.utcnow()            # Use ctx.block_time
uuid.uuid4()                 # Non-deterministic
random.random()              # Must seed first with random.seed()
open("/path", "w")           # No filesystem write
eval("dynamic code")         # No dynamic code execution
exec("dynamic code")         # No dynamic code execution

Cross-Contract Calls

Contracts can call other deployed contracts:

from panda import call_contract

@call
def delegate(self, ctx, other_contract: str, value: int):
    result = call_contract(
        address=other_contract,
        method="process",
        value=value,
    )
    self.state.last_result = result

Cross-contract calls cost a base fee of 1000 PCU plus the gas consumed by the called contract.

Testing

Local Testing

Use panda test to run contracts through PandaVM locally:

panda test my_contract.py

Writing Test Scripts

Create a test script that exercises your contract:

# test_counter.py
import json
import subprocess

def test_counter():
    # Deploy
    result = subprocess.run(
        ["panda", "test", "counter.py", "--method", "initialize"],
        capture_output=True, text=True
    )
    assert result.returncode == 0

    # Increment
    result = subprocess.run(
        ["panda", "test", "counter.py",
         "--method", "increment",
         "--args", '{"amount": 5}',
         "--state", '{"count": 0, "owner": "test"}'],
        capture_output=True, text=True
    )
    output = json.loads(result.stdout)
    assert output["state"]["count"] == 5

Determinism Verification

PandaVM automatically verifies determinism by running contracts twice and comparing results:

panda test my_contract.py --verify-determinism

This runs the contract twice with identical inputs and asserts byte-for-byte equality of:

  • Output/return value
  • State after execution
  • Emitted events

Example Contracts

Token Contract

The PRC20Token (contracts/tokens/prc20_token.py) is the reference fungible token implementation. It uses panda.token.FungibleToken for core token logic and adds freeze/thaw authority:

from panda import contract, constructor, call, query, event
from panda.token import FungibleToken, TokenError


@contract
class PRC20Token:
    class State:
        token: dict = {}
        mint_authority: str = ""
        freeze_authority: str = ""
        frozen: dict = {}

    def _t(self) -> FungibleToken:
        return FungibleToken(self.state.token)

    def _not_frozen(self, addr: str):
        if self.state.frozen.get(addr, False):
            raise TokenError(f"frozen: {addr}")

    @constructor
    def deploy(self, ctx, name: str, symbol: str, decimals: int = 18,
               max_supply: int = 0, initial_supply: int = 0):
        """Runs at deploy; pass matching JSON to panda deploy --args."""
        t = self._t()
        if t.name() or t.total_supply() > 0:
            raise TokenError("already initialized")
        t.configure(name=name, symbol=symbol, decimals=decimals, max_supply=max_supply)
        self.state.mint_authority = ctx.sender
        self.state.freeze_authority = ctx.sender
        if initial_supply > 0:
            t.mint(ctx.sender, initial_supply)
        self.state.token = t.to_dict()
        self.emit(
            event.PRC20Initialized(
                deployer=ctx.sender, name=name, symbol=symbol,
                decimals=decimals, initial_supply=initial_supply,
            )
        )

    @call
    def transfer(self, ctx, to: str, value: int):
        self._not_frozen(ctx.sender)
        self._not_frozen(to)
        t = self._t()
        t.transfer(ctx.sender, to, value)
        self.state.token = t.to_dict()
        self.emit(event.Transfer(_from=ctx.sender, _to=to, _value=value))

    @call
    def approve(self, ctx, spender: str, value: int):
        self._not_frozen(ctx.sender)
        t = self._t()
        t.approve(ctx.sender, spender, value)
        self.state.token = t.to_dict()
        self.emit(event.Approval(_owner=ctx.sender, _spender=spender, _value=value))

    @call
    def mint(self, ctx, to: str, amount: int):
        if ctx.sender != self.state.mint_authority:
            raise TokenError("not mint authority")
        t = self._t()
        t.mint(to, amount)
        self.state.token = t.to_dict()
        self.emit(event.Transfer(
            _from="0x0000000000000000000000000000000000000000",
            _to=to, _value=amount,
        ))

    @query
    def balance_of(self, owner: str) -> int:
        return self._t().balance_of(owner)

    @query
    def total_supply(self) -> int:
        return self._t().total_supply()

    @query
    def allowance(self, owner: str, spender: str) -> int:
        return self._t().allowance(owner, spender)

Prediction Market

The PredictionMarket (contracts/examples/prediction_market.py) is a binary prediction market where users bet on YES or NO outcomes, with proportional payouts after resolution:

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


@contract
class PredictionMarket:
    """Binary prediction market where users bet on YES or NO outcomes."""

    class State:
        question: str = ""
        owner: str = ""
        deadline: int = 0
        resolved: bool = False
        outcome: str = ""
        yes_pool: int = 0
        no_pool: int = 0
        bets: dict = {}
        claimed: dict = {}

    @constructor
    def deploy(self, ctx, question: str, deadline: int):
        if not question:
            raise ValueError("Question cannot be empty")
        if deadline <= ctx.block_time:
            raise ValueError("Deadline must be in the future")
        self.state.question = question
        self.state.owner = ctx.sender
        self.state.deadline = deadline

    @call
    def place_bet(self, ctx, side: str, amount: int):
        if self.state.resolved:
            raise ValueError("Market already resolved")
        if ctx.block_time >= self.state.deadline:
            raise ValueError("Betting period has ended")
        if side not in ("yes", "no"):
            raise ValueError("Side must be 'yes' or 'no'")
        if amount <= 0:
            raise ValueError("Amount must be positive")

        addr = ctx.sender
        bets = dict(self.state.bets)
        if addr in bets:
            raise ValueError("Already placed a bet")

        bets[addr] = {"side": side, "amount": amount}
        self.state.bets = bets

        if side == "yes":
            self.state.yes_pool = self.state.yes_pool + amount
        else:
            self.state.no_pool = self.state.no_pool + amount

        self.emit(event.BetPlaced(
            bettor=addr, side=side, amount=amount,
            yes_pool=self.state.yes_pool, no_pool=self.state.no_pool,
        ))

    @call
    def resolve(self, ctx, outcome: str):
        if ctx.sender != self.state.owner:
            raise ValueError("Only the owner can resolve")
        if self.state.resolved:
            raise ValueError("Already resolved")
        if ctx.block_time < self.state.deadline:
            raise ValueError("Cannot resolve before deadline")
        if outcome not in ("yes", "no"):
            raise ValueError("Outcome must be 'yes' or 'no'")

        self.state.resolved = True
        self.state.outcome = outcome
        self.emit(event.MarketResolved(
            outcome=outcome,
            yes_pool=self.state.yes_pool,
            no_pool=self.state.no_pool,
        ))

    @call
    def claim_payout(self, ctx):
        if not self.state.resolved:
            raise ValueError("Market not resolved yet")

        addr = ctx.sender
        bets = dict(self.state.bets)
        claimed = dict(self.state.claimed)

        if addr not in bets:
            raise ValueError("No bet placed")
        if addr in claimed:
            raise ValueError("Already claimed")

        bet = bets[addr]
        if bet["side"] != self.state.outcome:
            raise ValueError("You lost this bet")

        total_pool = self.state.yes_pool + self.state.no_pool
        winning_pool = self.state.yes_pool if self.state.outcome == "yes" else self.state.no_pool
        payout = (bet["amount"] * total_pool) // winning_pool

        claimed[addr] = True
        self.state.claimed = claimed

        self.emit(event.PayoutClaimed(
            bettor=addr, payout=payout, bet_amount=bet["amount"],
        ))
        return payout

    @query
    def get_market(self) -> dict:
        return {
            "question": self.state.question,
            "deadline": self.state.deadline,
            "resolved": self.state.resolved,
            "outcome": self.state.outcome,
            "yes_pool": self.state.yes_pool,
            "no_pool": self.state.no_pool,
            "total_bets": len(self.state.bets),
        }

    @query
    def get_odds(self) -> dict:
        total = self.state.yes_pool + self.state.no_pool
        if total == 0:
            return {"yes_pct": 50, "no_pct": 50}
        return {
            "yes_pct": (self.state.yes_pool * 100) // total,
            "no_pct": (self.state.no_pool * 100) // total,
        }

On-Chain ML Pipeline

The FraudDetector (contracts/examples/fraud_detector.py) trains a logistic regression classifier on-chain and uses it to flag suspicious transactions in real time:

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


@contract
class FraudDetector:
    """Logistic regression classifier for on-chain fraud detection."""

    class State:
        owner: str = ""
        model: dict = {}
        is_trained: bool = False
        model_version: int = 0
        total_predictions: int = 0
        flagged_count: int = 0
        threshold: float = 0.5

    @constructor
    def deploy(self, ctx, threshold: float = 0.5):
        if threshold <= 0.0 or threshold >= 1.0:
            raise ValueError("Threshold must be between 0 and 1")
        self.state.owner = ctx.sender
        self.state.threshold = threshold

    @call
    def train(self, ctx, features: list, labels: list):
        if ctx.sender != self.state.owner:
            raise ValueError("Only owner can train")
        if len(features) != len(labels):
            raise ValueError("features and labels must have same length")
        for label in labels:
            if label not in (0, 1):
                raise ValueError("Labels must be 0 or 1")

        model = LogisticRegression()
        model.fit(features, labels)
        self.state.model = save_model(model)
        self.state.is_trained = True
        self.state.model_version = self.state.model_version + 1

        self.emit(event.ModelTrained(
            version=self.state.model_version,
            sample_count=len(features),
            fraud_count=sum(labels),
            trainer=ctx.sender,
        ))

    @call
    def check_transaction(self, ctx, features: list):
        if not self.state.is_trained:
            raise ValueError("Model not trained yet")
        model = load_model(self.state.model)
        probas = model.predict_proba([features])
        proba = probas[0]
        fraud_score = round(proba[1], 6) if len(proba) > 1 else round(proba[0], 6)
        flagged = fraud_score >= self.state.threshold

        self.state.total_predictions = self.state.total_predictions + 1
        if flagged:
            self.state.flagged_count = self.state.flagged_count + 1

        self.emit(event.TransactionChecked(
            checker=ctx.sender, flagged=flagged, score=fraud_score,
        ))
        return {"flagged": flagged, "score": fraud_score}

    @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)
        probas = model.predict_proba(features)
        results = []
        for proba in probas:
            fraud_score = round(proba[1], 6) if len(proba) > 1 else round(proba[0], 6)
            results.append({
                "flagged": fraud_score >= self.state.threshold,
                "score": fraud_score,
            })
        return results

    @query
    def stats(self) -> dict:
        return {
            "is_trained": self.state.is_trained,
            "model_version": self.state.model_version,
            "threshold": self.state.threshold,
            "total_predictions": self.state.total_predictions,
            "flagged_count": self.state.flagged_count,
        }

Deployment Checklist

Before deploying a contract to testnet or mainnet:

  1. Run panda lint contract.py -- ensure no warnings or errors
  2. Run panda test contract.py -- verify all methods work correctly
  3. Run panda test contract.py --verify-determinism -- confirm deterministic execution
  4. Review gas costs with panda estimate contract.py --method <name> --args <json>
  5. Test on local Kind cluster first (make k8s-local)
  6. Deploy to testnet before mainnet
  7. Verify the contract in the block explorer