arrow-left

Only this pageAll pages
gitbookPowered by GitBook
triangle-exclamation
Couldn't generate the PDF for 254 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

General

Overview

Loading...

Loading...

Loading...

Loading...

Loading...

Getting Started

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Tutorials

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Seismic Solidity

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Clients

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

SRC20: Private Token

\Build a private ERC20 token where balances and transfers are hidden from observers

In this tutorial, you will build a fully functional private ERC20 token -- an SRC20 -- where balances, transfer amounts, and allowances are all shielded from external observers. Anyone watching the chain sees 0x00...0 instead of actual values, yet the token behaves like a standard ERC20 from the user's perspective.

hashtag
What you'll build

By the end of this tutorial you will have:

  • An SRC20 smart contract with shielded balances, transfers, and allowances

  • Encrypted transfer events that only the sender and recipient can decrypt

  • A signed-read pattern that lets users check their own balance without anyone else knowing it

  • Compliance-ready access control through Intelligence Contracts

  • A React frontend that connects everything end-to-end

hashtag
What makes this special

The contract changes from a standard ERC20 to an SRC20 are remarkably small. The core of it is changing uint256 to suint256 for balances, amounts, and allowances. The transfer logic, require checks, and overall structure stay almost identical. Seismic's compiler handles the rest -- routing reads and writes through shielded storage automatically.

This is the power of Seismic's approach: privacy is a type-level annotation, not a protocol-level rewrite.

hashtag
Prerequisites

Before starting, make sure you have:

  • Seismic development tools installed -- sforge, sanvil, and ssolc. See the if you have not set these up yet.

  • Solidity familiarity -- You should be comfortable writing and reading Solidity contracts.

hashtag
What you'll learn

Chapter
Topic
Key concept

hashtag
Tutorial structure

Chapter 1 starts with a side-by-side comparison of a standard ERC20 and the SRC20 version, walking through every changed line. Chapter 2 dives into the implementation of shielded transfers, allowances, and minting, including how to test with sforge. Chapter 3 tackles encrypted events -- since shielded types cannot appear in event parameters, you will use AES-GCM precompiles to encrypt sensitive data before emitting. Chapter 4 introduces signed reads, the mechanism that lets users query their own balance without exposing it. Chapter 5 adds compliance through Intelligence Contracts, showing how authorized roles can inspect shielded state. Chapter 6 brings it all together with a React frontend using seismic-react.

Each chapter builds on the previous one. By the end, you will have a complete, deployable private token with a working frontend.

Privy

Set up Privy with Seismic for email and social login

Privy provides email, social, and embedded wallet authentication. This guide shows how to integrate Privy with Seismic React for apps that need flexible onboarding beyond browser extension wallets.

hashtag
Prerequisites

npm install @privy-io/react-auth @privy-io/wagmi wagmi viem @tanstack/react-query seismic-react seismic-viem
circle-info

You need a Privy App ID from the Privy Dashboardarrow-up-right.

hashtag
Step 1: Configure Privy with Seismic Chain

Define the Seismic chain configuration for Privy:

hashtag
Step 2: Set Up wagmi Config

Create a wagmi config using Privy's wagmi integration:

hashtag
Step 3: Set Up Providers

Nest the providers in the correct order -- Privy wraps wagmi, and ShieldedWalletProvider goes inside:

circle-info

Privy's embedded wallets work seamlessly with Seismic -- users don't need a browser extension. Privy creates a wallet automatically when users sign in with email or social accounts.

hashtag
Step 4: Add Login Button

Use Privy's hooks to trigger the login modal:

hashtag
Step 5: Use Shielded Hooks

Once authenticated, use seismic-react hooks as normal:

hashtag
Complete Example

hashtag
See Also

  • -- Comparison of wallet libraries

  • -- Provider reference and options

  • -- Access shielded wallet context

-- Traditional wallet modal alternative
  • AppKit Guide -- WalletConnect modal alternative

  • Wallet Guides Overview
    ShieldedWalletProvider
    useShieldedWallet
    RainbowKit Guide
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    // Privy needs the chain in viem format
    const seismicChain = {
      id: seismicTestnet.id,
      name: seismicTestnet.name,
      nativeCurrency: seismicTestnet.nativeCurrency,
      rpcUrls: seismicTestnet.rpcUrls,
      blockExplorers: seismicTestnet.blockExplorers,
    };
    import { createConfig } from "@privy-io/wagmi";
    import { http } from "viem";
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    const wagmiConfig = createConfig({
      chains: [seismicTestnet],
      transports: {
        [seismicTestnet.id]: http(),
      },
    });
    import { PrivyProvider } from '@privy-io/react-auth'
    import { WagmiProvider } from '@privy-io/wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider } from 'seismic-react'
    
    const queryClient = new QueryClient()
    
    function App({ children }: { children: React.ReactNode }) {
      return (
        <PrivyProvider
          appId="YOUR_PRIVY_APP_ID"
          config={{
            defaultChain: seismicChain,
            supportedChains: [seismicChain],
            embeddedWallets: {
              createOnLogin: 'users-without-wallets',
            },
          }}
        >
          <QueryClientProvider client={queryClient}>
            <WagmiProvider config={wagmiConfig}>
              <ShieldedWalletProvider config={wagmiConfig}>
                {children}
              </ShieldedWalletProvider>
            </WagmiProvider>
          </QueryClientProvider>
        </PrivyProvider>
      )
    }
    import { usePrivy } from '@privy-io/react-auth'
    
    function LoginButton() {
      const { login, logout, authenticated, user } = usePrivy()
    
      if (authenticated) {
        return (
          <div>
            <p>Logged in as {user?.email?.address || user?.wallet?.address}</p>
            <button onClick={logout}>Log out</button>
          </div>
        )
      }
    
      return <button onClick={login}>Sign in</button>
    }
    import { useShieldedWallet } from 'seismic-react'
    
    function MyComponent() {
      const { walletClient, loaded, error } = useShieldedWallet()
    
      if (!loaded) return <div>Initializing shielded wallet...</div>
      if (error) return <div>Error: {error}</div>
      if (!walletClient) return <div>Sign in to get started.</div>
    
      return <div>Shielded wallet ready!</div>
    }
    'use client'
    
    import { PrivyProvider, usePrivy } from '@privy-io/react-auth'
    import { WagmiProvider, createConfig } from '@privy-io/wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider, useShieldedWallet } from 'seismic-react'
    import { seismicTestnet } from 'seismic-react/rainbowkit'
    import { http } from 'viem'
    
    const seismicChain = {
      id: seismicTestnet.id,
      name: seismicTestnet.name,
      nativeCurrency: seismicTestnet.nativeCurrency,
      rpcUrls: seismicTestnet.rpcUrls,
      blockExplorers: seismicTestnet.blockExplorers,
    }
    
    const wagmiConfig = createConfig({
      chains: [seismicTestnet],
      transports: {
        [seismicTestnet.id]: http(),
      },
    })
    
    const queryClient = new QueryClient()
    
    function LoginButton() {
      const { login, logout, authenticated, user } = usePrivy()
    
      if (authenticated) {
        return (
          <div>
            <p>Logged in as {user?.email?.address || user?.wallet?.address}</p>
            <button onClick={logout}>Log out</button>
          </div>
        )
      }
    
      return <button onClick={login}>Sign in</button>
    }
    
    function WalletStatus() {
      const { walletClient, publicClient, loaded, error } = useShieldedWallet()
    
      if (!loaded) return <p>Initializing shielded wallet...</p>
      if (error) return <p>Error: {error}</p>
      if (!walletClient) return <p>Sign in to get started.</p>
    
      return (
        <div>
          <p>Shielded wallet ready</p>
          <p>Public client: {publicClient ? 'Available' : 'Loading...'}</p>
        </div>
      )
    }
    
    export default function App() {
      return (
        <PrivyProvider
          appId="YOUR_PRIVY_APP_ID"
          config={{
            defaultChain: seismicChain,
            supportedChains: [seismicChain],
            embeddedWallets: {
              createOnLogin: 'users-without-wallets',
            },
          }}
        >
          <QueryClientProvider client={queryClient}>
            <WagmiProvider config={wagmiConfig}>
              <ShieldedWalletProvider config={wagmiConfig}>
                <LoginButton />
                <WalletStatus />
              </ShieldedWalletProvider>
            </WagmiProvider>
          </QueryClientProvider>
        </PrivyProvider>
      )
    }
    Basic understanding of ERC20 -- You should know what balanceOf, transfer, approve, and transferFrom do.

    AES-GCM precompiles for private event data

    4

    Letting users view their own balance securely

    5

    Compliance-compatible access control

    6

    React integration with seismic-react

    1

    ERC20 to SRC20

    Shielded types and the minimal diff

    2

    Shielded Balances and Transfers

    suint256 in practice, testing with sforge

    Installation guide

    3

    Encrypted Events
    Signed Reads
    Intelligence Contracts
    Building the Frontend

    Why Seismic

    As shown on the welcome page, shielding a Solidity contract can be as simple as adding an s prefix to your types. But why does that matter?

    hashtag
    The transparency problem

    Blockchains are public ledgers. Every balance, every transaction, every contract interaction is visible to anyone with a block explorer. This transparency was a design choice -- but it creates real problems:

    • Front-running and MEV extraction. Bots watch the mempool, see your trade, and sandwich it for profit. On Ethereum, MEV extraction costs users billions annually.

    • Competitive intelligence leaks. If your protocol holds assets on-chain, competitors can see your treasury, your positions, and your strategy in real time.

    • Privacy violations for end users. When Alice sends tokens to Bob, everyone can see how much she holds, how much she sent, and build a complete transaction graph linking her activity.

    • Business logic exposure. Contract state is fully readable. Pricing algorithms, liquidation thresholds, and internal parameters are all public.

    Traditional finance operates with confidentiality by default. Public blockchains flip that assumption, and it limits what you can build.

    hashtag
    Why existing privacy solutions fall short

    Several projects have tried to solve this. None of them let you stay in Solidity.

    Approach
    Limitation

    Each of these approaches forces a tradeoff: learn a new language, accept limited functionality, or live with performance constraints.

    hashtag
    The Seismic approach

    Seismic solves on-chain privacy with two innovations working together:

    hashtag
    Shielded types in the compiler

    Seismic extends the Solidity compiler with shielded types: suint, sint, sbool, sbytes, and saddress. The s prefix marks a value as shielded. Under the hood, these compile to CLOAD and CSTORE opcodes (instead of the standard SLOAD/SSTORE), which route data through shielded storage.

    No new language. No new programming model. Just a one-letter prefix on the types you already know.

    hashtag
    TEE-based execution

    Seismic nodes run inside Trusted Execution Environments (TEEs) using Intel TDX. The TEE creates a hardware-enforced boundary: even the node operator cannot read the data being processed. Transactions are encrypted before they hit the network and decrypted only inside the TEE for execution.

    Together, these two layers provide privacy at the language level and enforcement at the hardware level.

    hashtag
    What does not change

    Seismic is designed so that everything around the privacy layer stays familiar:

    • Same language. Standard Solidity, with shielded types added. No new syntax beyond the s prefix. Existing Solidity contracts compile and deploy without modification.

    • Same tooling. sforge, sanvil, and ssolc are forks of Foundry's forge, anvil

    Welcome

    Private by Default. Familiar by Design.

    Seismic is an EVM blockchain with native on-chain privacy. Write Solidity. Deploy with Foundry. Interact with Viem. The only difference: your users' data stays private.


    hashtag
    One letter changes everything

    The difference between a public ERC20 and a private SRC20 is one letter:

    The s prefix tells the Seismic compiler to shield the underlying value. Observers see

    Computationally expensive. Operations on encrypted data are orders of magnitude slower, limiting what you can practically build.

    , and
    solc
    . Your workflow does not change.
  • Same client libraries. seismic-viem extends Viem. seismic-react extends Wagmi. seismic-alloy extends Alloy for Rust. seismic_web3 extends web3.py for Python.

  • Same standards. ERC20 becomes SRC20. The interface is the same. The deployment flow is the same. The difference is that balances are private.

  • Same deployment flow. Write, test, deploy. The commands are sforge build, sforge test, sforge create. If you have deployed to Ethereum, you can deploy to Seismic.

  • ZK chains (Aztec, Aleo, Mina)

    Require new languages (Noir, Leo, o1js). You leave Solidity, Foundry, and the entire EVM ecosystem behind.

    Mixers (Tornado Cash)

    Only work for transfers. You can obscure the sender, but contract state is still public. No programmable privacy.

    L2 privacy layers

    Sacrifice composability. Contracts on the privacy layer cannot natively interact with contracts on the base chain.

    Homomorphic encryption (fhEVM)

    Quickstart

    You're two commands away from running an encrypted protocol

    You can play around with shielded types using our starter repositoryarrow-up-right. This assumes you went through everything in Installation.

    git clone "https://github.com/SeismicSystems/seismic-starter.git"
    cd seismic-starter/packages/contracts
    sforge test -vv

    Or if you prefer a more detailed walkthrough of building on Seismic, check out our Tutorials section.

    0x00...0
    instead of actual balances and amounts. Everything else — the Solidity syntax, the EVM execution model, the deployment flow — stays exactly the same.

    hashtag
    What you can build

    • Shielded tokens — ERC20s where balances and transfer amounts are hidden from observers

    • Confidential DeFi — AMMs and lending protocols where positions, prices, and liquidation thresholds are shielded

    • Compliant finance — Privacy with built-in access control so regulators can verify without exposing user data

    • Private voting — On-chain governance where votes are secret until tallied


    hashtag
    3-minute quickstart

    Already have Rust and the Seismic tools installed?

    You just ran shielded contract tests locally. See the full quickstart for next steps.


    hashtag
    Find what you need

    I want to...
    Go to

    Understand why Seismic exists

    See how it works under the hood

    Set up my dev environment


    hashtag
    Pre-requisite knowledge

    Our documentation assumes some familiarity with blockchain app development. Before getting started, it'll help if you're comfortable with:

    • Solidityarrow-up-right

    • Foundryarrow-up-right

    • Viemarrow-up-right

    If you're new to blockchain app development or need a refresher, we recommend starting out with the CryptoZombiesarrow-up-right tutorial.


    hashtag
    Work with us

    If you might benefit from direct support from the team, please don't hesitate to reach out to [email protected]. We pride ourselves in fast response time.

    You can also check out our X accountarrow-up-right for the latest updates, or join our Discordarrow-up-right community.

    // Standard ERC20 — balances visible to everyone
    mapping(address => uint256) public balanceOf;
    
    function transfer(address to, uint256 amount) public {
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
    // Seismic SRC20 — balances shielded by default
    mapping(address => suint256) balanceOf;    // uint256 → suint256
    
    function transfer(address to, suint256 amount) public {  // uint256 → suint256
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
    git clone "https://github.com/SeismicSystems/seismic-starter.git"
    cd seismic-starter/packages/contracts
    sforge test -vv

    Verify devtool installation

    Before continuing, ensure that you have completed the steps in the Installation section to install all necessary Seismic developer tools:

    • sforge: Framework for testing and deploying smart contracts.

    • sanvil: Local Seismic node.

    • ssolc: The Seismic Solidity compiler.

    Run each of the above commands in your terminal with a --version flag to ensure that they're installed correctly.

    Also ensure that you have installed on your machine. If you do not have bun installed, follow the instructions to install it on your machine.

    Installation

    Setting up your local machine to develop with Seismic


    hashtag
    System requirements

    Before you begin, make sure your machine meets the following requirements:

    • x86_64 or arm64 architecture

    Web Interface

    Deploy SRC20 tokens from a browser using the SRC20 Factory web interface

    The SRC20 Factory ships a React web GUI (packages/web) for deploying tokens directly from a browser wallet, no CLI or code required.

    hashtag
    Running the web app

    Clone the repo and start the dev server:

    Initialize contracts

    1. Navigate to the contracts subdirectory:

    1. Initialize a project with sforge:

    This command will:

    Initialize the CLI

    Now that the contracts subdirectory has been initialized, you should now initialize the cli subdirectory that will be used to interact with the deployed contracts.

    1. Navigate to the CLI subdirectory:

    1. Initialize a new bun project

    Writing the Contract

    This section dives into the heart of Clown Beatdown — the shielded smart contract that powers its functionality. You'll start by building the secrets pool using shielded storage, then implement the stamina system and the robbery mechanic, before adding rounds, resets, and contributor-based access control. By the end of this section, you'll have a fully functional, round-based ClownBeatdown contract that is secure, fair, and replayable.

    hashtag
    What You'll Learn

    In this section, you'll:

    Ch 1: The Secrets Pool

    In this chapter, you'll learn to create and initialize the secrets pool — a collection of hidden strings stored inside the clown's pockets — and implement a function to add new secrets. Estimated time: ~10 minutes.

    hashtag
    Defining the secrets pool

    The secrets pool is the collection of hidden strings that the clown carries. Using Seismic's sbytes type, each secret is shielded on-chain — encrypted and invisible to observers. A shielded suint256 index determines which secret gets revealed when the clown is robbed. Open up

    TypeScript

    TypeScript client libraries for Seismic

    Seismic provides two TypeScript packages:

    Package
    Purpose
    Use when
    Define a pool of shielded secrets using sbytes and a shielded index using suint256.
  • Build the stamina bar, the protective layer that guards the secrets, and implement a hit() function to reduce it.

  • Add a rob() function to reveal a randomly selected secret to authorized contributors.

  • Implement a reset mechanism to restart the game for multiple rounds.

  • Track player contributions in each round, ensuring that only contributors can rob the clown.

  • hashtag
    Overview of Chapters

    • Chapter 1: The Secrets Pool

    You'll define the secrets pool using shielded storage (sbytes) and a shielded index (suint256), and implement an addSecret() function to populate it. This chapter introduces shielded writes.

    • Chapter 2: The Stamina Bar and Robbing Secrets

    Learn how to build the stamina system, which protects the secrets from being accessed prematurely. You'll implement the hit() function to reduce stamina and the rob() function to reveal a secret once conditions are met.

    • Chapter 3: Reset Mechanism, Rounds, and Contributor Access

    This chapter introduces a reset mechanism to enable multiple rounds of gameplay. You'll track contributions per round and ensure that only players who helped knock out the clown in a specific round can rob it. This chapter introduces signed reads.

    bunarrow-up-right
    herearrow-up-right

    Run my first shielded contract

    Quickstart

    Build a complete app step by step

    Clown Beatdown Tutorial

    Build a shielded ERC20 token

    SRC20 Tutorial

    Learn about shielded types

    Shielded Types

    Integrate a frontend

    Client Libraries

    Deploy to testnet

    Migrating from Ethereum

    Understand the transaction lifecycle

    Seismic Transaction

    Run a node

    Node Operator FAQ

    Why Seismic
    How Seismic Works
    Installation

    MacOS, Ubuntu, or Windows (other Linux distros may work but are not officially tested)


    hashtag
    Install the local development suite

    The local development suite uses sforge as the testing framework, sanvil as the local node, and ssolc as the compiler.

    1. Install rustarrow-up-right and cargoarrow-up-right on your machine if you don't already have them. Default installation works well.

    1. Download and execute the sfoundryup installation script.

    1. Install sforge, sanvil, ssolc. Expect this to take between 5-20 minutes depending on your machine.

    1. (Optional) Remove old build artifacts in existing projects. You can ignore this step if you aren't working with existing foundry projects.


    hashtag
    Set up the VSCode extension

    We recommend adding syntax highlighting via the seismicarrow-up-right (or seismicarrow-up-right for Open VSX) extension from the VSCode marketplace. If you already have the solidity extension, you'll have to disable it while writing Seismic code.

    hashtag
    Deploying a token
    1. Open the web app and connect MetaMask

    2. Switch to the Seismic testnet (chain ID 5124)

    3. Fill in the token name, symbol, and initial supply

    4. Click Deploy and confirm the transaction in your wallet

    5. The deployed token address and transaction hash appear on success

    circle-info

    Supply is entered in whole tokens. Entering 1000000 mints 1,000,000 × 10¹⁸ base units.

    hashtag
    Wagmi configuration

    The web app connects MetaMask to Seismic testnet via wagmi:

    Other wallet connectors (WalletConnect, Coinbase, etc.) are not configured by default but can be added following the wallet guides.

    Create the contract project structure (e.g., src/, test/, foundry.toml).

  • Automatically install the Forge standard library (forge-std) as a submodule.

  • Remove the .github workflow folder (not required)

    1. Edit the .gitignore file to be the following:

    1. Delete the default contract, test and script files (Counter.sol and Counter.t.sol and Counter.s.sol) and replace them with their ClownBeatdown counterparts (ClownBeatdown.sol, ClownBeatdown.t.sol and ClownBeatdown.s.sol):

    These files are empty for now, but we will add to them as we go along.

    cd packages/contracts

    Now, create an src/ folder and move index.ts there.

    1. Now, edit package.json to be the following:

    1. Edit .gitignore to be:

    Your environment is now set!

    # Assuming you are currently in the contracts directory
    cd ../cli
    bun init -y
    packages/contracts/src/ClownBeatdown.sol
    and define the state variables:

    We accept _clownStamina in the constructor but won't use it until Chapter 2, when we add the stamina system.

    hashtag
    Add the addSecret function

    Next, let's implement a function to add secrets to the pool. The addSecret function takes a plain string and converts it to sbytes for shielded storage:

    hashtag
    Add the random index helper

    The _randomIndex function generates a pseudo-random index into the secrets array using on-chain randomness sources:

    hashtag
    What's happening here?

    The addSecret function converts a plain string into sbytes (shielded bytes) and stores it in the secrets mapping. Because sbytes is a shielded type, the secret's contents are encrypted on-chain and invisible to observers.

    The function also updates shielded state (secretIndex is a suint256), which makes a call to this function a shielded write.

    Note that we're using two different shielded types here:

    • sbytes — for storing encrypted strings (the secrets themselves)

    • suint256 — for storing an encrypted integer (the index that determines which secret gets revealed)

    curl https://sh.rustup.rs -sSf | sh
    curl -L \
         -H "Accept: application/vnd.github.v3.raw" \
         "https://api.github.com/repos/SeismicSystems/seismic-foundry/contents/sfoundryup/install?ref=seismic" | bash
    source ~/.zshenv  # or ~/.bashrc, depending on your shell
    sfoundryup
    source ~/.zshenv  # or ~/.bashrc, depending on your shell
    sforge clean  # run in your project's contract directory
    bun install
    bun run dev:web
    import { createConfig, http } from "wagmi";
    import { injected } from "wagmi/connectors";
    import { seismicTestnet } from "seismic-viem";
    
    export const wagmiConfig = createConfig({
      chains: [seismicTestnet],
      connectors: [injected({ target: "metaMask" })],
      transports: {
        [seismicTestnet.id]: http(),
      },
    });
    sforge init && rm -rf .github
    .env
    broadcast/
    cache/
    # Remove the Counter files
    rm -f src/Counter.sol test/Counter.t.sol script/Counter.s.sol
    
    # Create empty ClownBeatdown files in the same locations
    touch src/ClownBeatdown.sol test/ClownBeatdown.t.sol script/ClownBeatdown.s.sol
    mkdir -p src && mv index.ts src/
    {
      "name": "clown-beatdown-cli",
      "license": "MIT License",
      "type": "module",
      "scripts": {
        "dev": "bun run src/index.ts"
      },
      "dependencies": {
        "dotenv": "^16.4.7",
        "seismic-viem": "1.1.1",
        "viem": "^2.22.3"
      },
      "devDependencies": {
        "@types/node": "^22.7.6",
        "typescript": "^5.6.3"
      }
    }
    node_modules
    // SPDX-License-Identifier: MIT License
    pragma solidity ^0.8.13;
    
    contract ClownBeatdown {
        mapping(uint256 => sbytes) secrets; // Pool of possible secrets (shielded).
        uint256 secretsCount; // Number of secrets for modular arithmetic.
        suint256 secretIndex; // Shielded index into the secrets mapping.
        uint256 round; // The current round number (used by _randomIndex).
    
        constructor(uint256 _clownStamina) {
            round = 1; // Start with the first round.
        }
    }
    function addSecret(string memory _secret) public {
        secrets[secretsCount] = sbytes(_secret);
        secretsCount++;
        secretIndex = suint256(_randomIndex()); // Re-pick a random secret.
    }
    // Generate a pseudo-random index into the secrets array.
    function _randomIndex() private view returns (uint256) {
        return uint256(keccak256(abi.encodePacked(block.prevrandao, block.timestamp, round))) % secretsCount;
    }

    React hooks layer built on seismic-viem + wagmi

    React applications

    seismic-react depends on seismic-viem — install both if you're building a React app, or just seismic-viem for everything else.

    seismic-viem

    Low-level SDK built on viem 2.x

    Server-side, scripts, non-React apps

    REST API

    Query SRC20 tokens deployed through the factory via a REST API

    The src20-factory repo includes a Rust API server (Axum) that reads factory and token state over HTTP. It is useful for indexing deployed tokens or fetching token metadata from non-TypeScript environments.

    hashtag
    Running the server

    cd packages/api
    cargo run

    The server starts on port 3001 and connects to Seismic testnet at https://testnet-2.seismictest.net/rpc.


    hashtag
    GET /api/tokens

    Returns all tokens ever deployed through the factory, with their metadata.

    hashtag
    Request

    hashtag
    Response

    circle-info

    Addresses are returned as lowercase hex, not EIP-55 checksummed.

    Note that total_supply is always in base units (not scaled by decimals).


    hashtag
    GET /api/token/{address}

    Returns metadata for a single token by address.

    hashtag
    Request

    hashtag
    Response

    hashtag
    Error response

    If the address is invalid or the RPC call fails, the server returns a JSON error:


    hashtag
    What the API can and cannot read

    The API uses a standard public provider — it does not hold a private key. This means it can read public state like name, symbol, decimals, owner, and totalSupply, but it cannot read shielded state like individual balances or allowances, which require a signed read from the balance holder. See for how those work.

    Seismic-viem Primer

    Before proceeding to write the CLI, you need to be acquainted with some of the functions and utilities used to enable Seismic primitives (e.g. shielded reads, shielded writes etc.) through our client library, seismic-viem, which we will be using heavily to write the CLI. The detailed docs for seismic-viem can be found here. Estimated time: ~15 minutes

    hashtag
    Shielded wallet client

    The shielded wallet client is the shielded/Seismic counterpart of the wallet client in viem. It is used to enable extended functionality for interacting with shielded blockchain features, wallet operations, and encryption. It can be initialized using the createShieldedWalletClient function as follows:

    createShieldedWalletClient takes in the following parameters:

    1. chain: a well-defined object

    2. transport: the method of transport of interacting with the chain (http/ws along with the corresponding RPC URL)

    Once initialized, it can then be used to perform wallet operations or shielded-specific actions.

    hashtag
    Shielded contract

    A shielded contract instance provides an interface to interact with a shielded contract onchain. It has extended functionality for performing shielded write operations, signed reads, and contract interaction for a specific contract performed by a specific wallet client that it is initialized with. It can be initialized with getShieldedContract as follows:

    It takes in the following parameters:

    1. abi: the ABI of the contract it is interacting with.

    2. address: the address of the deployed contract it is interacting with.

    3. client: the shielded wallet client that the interactions are to be performed by.

    This function extends the base getContract functionality by adding:

    • Shielded write actions for nonpayable and payable functions.

    • Signed read actions for pure and view functions.

    We will extensively use shielded writes (for addSecret) and signed reads (for rob()) in our CLI.

    Collections

    Using stype variables in arrays and maps

    hashtag
    Shielded Arrays

    Arrays of shielded types work like standard Solidity arrays. Keys (indices) must be non-shielded — using a shielded type as an array index is a compiler error.

    They come in two forms:

    • Dynamic (suint256[], sbool[], saddress[], etc.) — the length is stored as shielded.

    • Fixed-size (suint256[5], sbool[4], saddress[3], etc.) — the length is a compile-time constant and publicly visible.

    circle-exclamation

    Even with dynamic shielded arrays, an upper bound on the length may be visible to observers monitoring gas costs, since gas usage scales with array operations.

    hashtag
    Shielded Mappings

    Mappings can have shielded values but keys cannot be shielded types. Using a shielded type as a mapping key is a compiler error. The standard mapping syntax applies.

    Overview

    Seismic maintains client libraries for three languages.

    hashtag
    TypeScript

    hashtag
    seismic-viem

    composes with to add Seismic transaction support, encrypted calldata, and signed reads. See the .

    hashtag
    seismic-react

    composes with to provide React hooks for shielded reads, writes, and wallet management. See the .

    hashtag
    Python — seismic-web3

    composes with to interact with Seismic nodes from Python. See the .

    hashtag
    Rust — seismic-alloy

    composes with to provide Seismic transaction types and encryption-aware providers. See the .

    circle-exclamation

    The docs for all three of these libraries are pure AI slop. The Python docs have been manually reviewed and cleaned up; we're auditing the TypeScript and Rust docs shortly. Refer to the source code if anything seems off.

    Building the Frontend

    In this section, you'll build a React frontend for the Clown Beatdown game. Players connect their wallet, punch the clown to reduce its stamina, and rob a shielded secret once it's knocked out — all from the browser. Estimated time: ~45 minutes

    The frontend uses seismic-react to integrate shielded wallet functionality with a standard React + wagmi + RainbowKit stack. By the end of this section, you'll have a fully playable web app running against your local Seismic node.

    hashtag
    What You'll Learn

    • Set up a React + Vite project with seismic-react, wagmi, and RainbowKit

    • Configure providers for shielded wallet support

    • Build custom hooks to interact with the ClownBeatdown contract

    • Create game UI components with animations and responsive layout

    hashtag
    Overview of Chapters

    Install dependencies, configure Vite, and wire up the provider stack: WagmiProvider, RainbowKitProvider, and ShieldedWalletProvider from seismic-react.

    Build the hooks that connect your UI to the ClownBeatdown contract — useContract, useContractClient, and useGameActions.

    Create the game interface: the clown sprite with punch animations, action buttons (hit, rob, reset), and the entry screen with wallet connection.

    Create project structure

    1. Create the project folder and navigate into it:

    1. Create the packages directory with subdirectories for contracts and cli

    Best Practices

    These are guidelines for writing secure Seismic contracts. Following them will help you avoid the most common privacy mistakes.

    hashtag
    Always Use Seismic Transactions for Shielded Parameters

    Any function that accepts shielded types as parameters should be called using a Seismic transaction (type 0x4A), which encrypts the calldata. If calldata is not encrypted, the shielded parameter values are visible in plaintext in the transaction's input data, defeating the purpose of using shielded types. This applies to all function calls that pass shielded values as parameters.

    Seismic Testnet

    Seismic public testnet chain configuration

    The Seismic public testnet is the primary network for development and testing. seismicTestnet is a RainbowKit-compatible chain object that wraps the seismic-viem testnet definition with RainbowKit metadata.

    hashtag
    Configuration

    Property

    Examples

    Complete runnable examples for Seismic React

    Complete working examples demonstrating common Seismic React patterns. Each example is self-contained and can be copied into a new project.

    hashtag
    Available Examples

    Example
    Description
    seismic-viemarrow-up-right
    viemarrow-up-right
    full documentation
    seismic-reactarrow-up-right
    wagmiarrow-up-right
    full documentation
    seismic-web3arrow-up-right
    web3.pyarrow-up-right
    full documentation
    seismic-alloyarrow-up-right
    alloyarrow-up-right
    full documentation
    Ch 1: Project Setup and Providers
    Ch 2: Contract Hooks
    Ch 3: Game UI Components
    seismic-react
    Signed Reads
    privateKey: the private key to create the client for
    Proxy-based access to dynamically invoke contract methods.
    Chainarrow-up-right
    suint256[] private balances;     // dynamic — shielded length
    sbool[4] private flags;          // fixed — length 4 is public
    
    function example(uint256 i) public {
        balances[i] = suint256(100);   // valid — uint256 index
        flags[0] = sbool(true);        // valid — literal index
    }
    The contracts subdirectory will house the Seismic smart contract(s) and test(s) for the project, while the cli will house the interface to interact with the contracts.
    1. Initialize a bun project in the root directory:

    We remove the default index.ts and tsconfig.json files created by bun init -y to keep the root directory clean and focused on managing the monorepo structure rather than containing code. We also create a .prettierrc file for consistent code formatting and a .gitmodules file to manage contract submodules.

    1. Replace the default package.json with the following content for a monorepo setup:

    1. Add the following to the .prettierrc file for consistent code formatting:

    1. Replace the .gitignore file with:

    1. Add the following to the .gitmodules file to track git submodules (in our case, only the Forge standard library, forge-std):

    Your client library (e.g., seismic-viem) handles Seismic transaction construction automatically when you use shielded write functions.

    hashtag
    Be Mindful of Gas-Based Information Leakage

    Gas consumption is publicly visible. Any operation whose gas cost depends on a shielded value is a potential leak. The main offenders are:

    • Conditional branches on shielded booleans (different branches use different gas).

    • Loops with shielded bounds (iteration count is visible via gas).

    • Exponentiation with shielded exponents (gas scales with exponent value).

    Use constant-time patterns: fixed-size loops, branchless logic, and avoid shielded exponents.

    CLOAD and CSTORE themselves have constant gas cost by design -- the risk comes from higher-level patterns.

    hashtag
    Use Casting Carefully

    Every cast between shielded and unshielded types is a potential information leak. See Casting for details.

    • Unshielded to shielded: The input value is visible in the trace.

    • Shielded to unshielded: The output value is visible in the trace.

    Minimize casts. During security review, identify every cast and confirm the exposure is intentional.

    hashtag
    Review Compiler Warnings

    The Seismic Solidity compiler generates a ton of warnings specific to shielded types. This is designed to be annoying because of how easy it is to make a mistake. Please review each warning carefully. These warnings flag potential privacy issues such as:

    • Shielded values used in contexts that may leak information.

    • Casts that expose confidential data.

    • Patterns known to be risky.

    Do not ignore these warnings. Treat them as seriously as you would treat security audit findings.

    hashtag
    Test with sforge test

    Use sforge test (the Seismic fork of Foundry's forge test) to run your test suite. It supports shielded types natively and can catch issues specific to confidential computation that standard forge test would miss.

    Write tests that specifically verify:

    • Shielded values remain confidential through the expected code paths.

    • Access control prevents unauthorized reads of unshielded data.

    hashtag
    Keep Shielded Data in the Shielded Domain

    The longer a value stays shielded, the more private it is. Avoid unnecessary round-trips between shielded and unshielded types. Perform as much computation as possible in the shielded domain before unshielding a final result (if unshielding is even needed at all).

    GET http://localhost:3001/api/tokens
    {
      "count": 2,
      "tokens": [
        {
          "address": "0xabc123...",
          "name": "My Private Token",
          "symbol": "MPT",
          "decimals": 18,
          "owner": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
          "total_supply": "1000000000000000000000000"
        },
        {
          "address": "0xdef456...",
          "name": "Another Token",
          "symbol": "AT",
          "decimals": 6,
          "owner": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
          "total_supply": "500000000000"
        }
      ]
    }
    GET http://localhost:3001/api/token/0xabc...
    {
      "name": "My Private Token",
      "symbol": "MPT",
      "decimals": 18,
      "owner": "0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266",
      "total_supply": "1000000000000000000000000"
    }
    {
      "error": "Invalid address: ..."
    }
    const walletClient = await createShieldedWalletClient({
      chain: seismicChain,
      transport: httpTransport,
      privateKey: "0xabcdef...",
    });
    const contract = getShieldedContract({
      abi: myContractAbi,
      address: "0x1234...",
      client: shieldedWalletClient,
    });
    // Perform a shielded write
    await contract.write.myFunction([arg1, arg2], { gas: 50000n });
    
    // Perform a signed read
    const value = await contract.read.getValue();
    console.log("Value:", value);
    mapping(address => suint256) private balances;    // valid
    mapping(uint256 => sbool) private flags;          // valid
    mapping(address => saddress) private recipients;  // valid
    
    mapping(saddress => uint256) private lookup;      // INVALID — shielded key
    mkdir clown-beatdown
    cd clown-beatdown
    mkdir -p packages/contracts packages/cli
    bun init -y && rm index.ts && rm tsconfig.json && touch .prettierrc && touch .gitmodules
    {
      "workspaces": ["packages/**"],
      "dependencies": {},
      "devDependencies": {
        "@trivago/prettier-plugin-sort-imports": "^5.2.1",
        "prettier": "^3.4.2"
      }
    }
    {
      "semi": false,
      "tabWidth": 2,
      "singleQuote": true,
      "printWidth": 80,
      "trailingComma": "es5",
      "plugins": ["@trivago/prettier-plugin-sort-imports"],
      "importOrder": [
        "<TYPES>^(?!@)([^.].*$)</TYPES>",
        "<TYPES>^@(.*)$</TYPES>",
        "<TYPES>^[./]</TYPES>",
        "^(?!@)([^.].*$)",
        "^@(.*)$",
        "^[./]"
      ],
      "importOrderParserPlugins": ["typescript", "jsx", "decorators-legacy"],
      "importOrderSeparation": true,
      "importOrderSortSpecifiers": true
    }
    # Compiler files
    cache/
    out/
    
    # Ignores development broadcast logs
    !/broadcast
    /broadcast/*/31337/
    /broadcast/**/dry-run/
    
    # Docs
    docs/
    
    # Dotenv file
    .env
    
    node_modules/
    [submodule "packages/contracts/lib/forge-std"]
    	path = packages/contracts/lib/forge-std
    	url = https://github.com/foundry-rs/forge-std
    // This function should ONLY be called via a Seismic transaction
    function deposit(suint256 amount) external {
        balances[msg.sender] += amount;
    }
    sforge test
    // BAD: Unnecessary unshielding and re-shielding
    suint256 a = /* ... */;
    suint256 b = /* ... */;
    uint256 temp = uint256(a) + uint256(b);  // Both values leaked in trace
    suint256 result = suint256(temp);
    
    // GOOD: Stay in the shielded domain
    suint256 a = /* ... */;
    suint256 b = /* ... */;
    suint256 result = a + b;  // No values leaked
    Value

    Chain ID

    5124

    Name

    Seismic

    RPC (HTTPS)

    https://testnet-1.seismictest.net/rpc

    RPC (WSS)

    wss://testnet-1.seismictest.net/ws

    Explorer

    https://seismic-testnet.socialscan.io

    hashtag
    Import

    hashtag
    Usage

    hashtag
    With RainbowKit

    hashtag
    With wagmi Config

    hashtag
    Notes

    • Chain ID 5124 is used for EIP-155 replay protection and EIP-712 typed data signing

    • The testnet supports all Seismic protocol features including shielded transactions and signed reads

    • The Seismic icon is included automatically for display in RainbowKit's chain selector

    hashtag
    See Also

    • Chains Overview - All supported chains

    • Sanvil - Local development chains

    • createSeismicDevnet - Custom chain factory

    • - RainbowKit setup guides

    import { seismicTestnet } from "seismic-react/rainbowkit";
    import { getDefaultConfig } from "@rainbow-me/rainbowkit";
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    const config = getDefaultConfig({
      appName: "My App",
      projectId: "YOUR_PROJECT_ID",
      chains: [seismicTestnet],
    });
    import { http, createConfig } from "wagmi";
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    const config = createConfig({
      chains: [seismicTestnet],
      transports: {
        [seismicTestnet.id]: http(),
      },
    });

    Complete minimal dApp: provider setup, connect wallet, shielded write, signed read

    hashtag
    Common Setup

    Every example uses the same provider wrapper that combines RainbowKit, wagmi, and Seismic:

    circle-info

    ShieldedWalletProvider must be nested inside WagmiProvider and QueryClientProvider. It automatically creates shielded clients when a wallet connects.

    hashtag
    Prerequisites

    • Node.js 18+

    • A WalletConnectarrow-up-right project ID

    • seismic-react and peer dependencies installed

    hashtag
    See Also

    • Hooks - Hook API reference

    • Wallet Guides - RainbowKit, AppKit, and Privy integration

    • Installation - Full dependency setup

    import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider } from 'seismic-react'
    import { config } from './config'
    
    const queryClient = new QueryClient()
    
    export function Providers({ children }: { children: React.ReactNode }) {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                {children}
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    npm install seismic-react seismic-viem wagmi viem @rainbow-me/rainbowkit @tanstack/react-query

    Differences from Ethereum

    hashtag
    Overview

    The Seismic EVMarrow-up-right is approximately a superset of the EVM

    hashtag
    What's the same

    • Transaction construction and serialization identical to Ethereum (with one new transaction type)

    • Address generation, gas estimation, and signing work the same as Ethereum

    • RPC methods are identical to reth

    • Standard Solidity bytecode will behave identically on Seismic, with (e.g., SSTORE reverts on shielded slots)

    • Seismic supports all of Ethereum's opcodes & precompiles

    • EIP-1559 transactions follow standard EIP-1559 fee rules. Seismic transactions (type 0x4A) use legacy fee pricing

    • Seismic will produce empty blocks when there are no pending transactions

    hashtag
    Key differences

    • Shielded storage: Solidity contracts can store private data on-chain

    • Runs in a TEE: Seismic nodes must run in Trusted Execution Environments

    • Seismic transaction: We added a new transaction type that allows you to encrypt your calldata

    hashtag
    EVM Compatibility

    hashtag
    Opcodes

    • – load shielded data from storage

    • – write shielded data to storage

    • – get the block timestamp in milliseconds. Note that block.timestamp still returns seconds, matching standard Solidity. Use block.timestamp_ms for millisecond precision

    hashtag
    Seismic transaction

    The transaction with type 0x4a allows users to encrypt their calldata. These otherwise work just like legacy transactions. We also support the other standard Ethereum transaction types (Legacy, EIP-1559, EIP-2930, EIP-4844, EIP-7702)

    hashtag
    Precompiles

    All standard Ethereum precompiles are still available. Seismic added 6 new to our EVM:

    • — securely generate a random number

    • — Elliptic Curve Diffie-Hellman, for generating a shared secret given a public key and secret key

    • — encrypt data with AES-GCM

    hashtag
    Staking

    Seismic uses the same staking contract as Ethereum, which is hardcoded into our Genesis block at address 0x00000000219ab540356cbb839cbe05303d7705fa

    hashtag
    Block times

    We will often produce multiple blocks in the same second, yet Ethereum's block timestamps are expressed in terms of unix seconds. Our solution to this:

    • Block headers and the EVM use timestamps in milliseconds internally

    • In Seismic Solidity, block.timestamp returns unix seconds, just like in standard Solidity. We added block.timestamp_ms which returns unix milliseconds. block.timestamp_seconds is an alias for block.timestamp

    hashtag
    RPC compatibility

    We support almost every RPC endpoint in Reth, and have added a few more of our own. See the full reference for details.

    Seismic-specific methods:

    • — returns the TEE's encryption public key for ECDH key exchange

    Modified Ethereum methods:

    • — zeroes the from field on unsigned calls; supports via type 0x4A

    • — accepts Seismic transaction type 0x4A with encrypted calldata

    Use Cases

    Seismic's shielded types let you add privacy to any smart contract pattern. Below are five categories with code snippets showing how each looks in Seismic Solidity.

    hashtag
    Private tokens (SRC20)

    An ERC20 with shielded balances and transfer amounts. Users can check their own balance through signed reads, but no one else can see it.

    mapping(address => suint256) balanceOf;
    mapping(address => mapping(address => suint256)) allowance;
    suint256 totalSupply;
    
    function transfer(address to, suint256 amount) public {
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }

    The only difference from a standard ERC20: uint256 becomes suint256. The compiler routes all balance reads and writes through shielded storage automatically. Observers see 0x00...0 for all balances and amounts.

    For a full walkthrough, see the .

    hashtag
    Confidential DeFi

    An AMM where liquidity positions, reserves, and swap amounts are hidden from observers. This prevents front-running and MEV extraction because bots cannot see the pool state or pending swaps.

    The constant-product formula is standard AMM logic. Shielding the reserves and amounts means no one can compute the price impact of a pending swap or sandwich a user's trade.

    hashtag
    Compliant finance (Intelligence Contracts)

    Contracts can optionally implement access control over shielded data — for example, exposing a view function that only authorized addresses can call. This pattern is sometimes called an "Intelligence Contract": a shielded contract that selectively reveals information to specific parties.

    In this example, balances are stored as suint256 so they are shielded by default. The getBalance function casts the shielded value to a regular uint256 for return, but only if the caller is the account owner or holds the COMPLIANCE_ROLE. Everyone else is rejected.

    hashtag
    Private voting

    Secret ballot governance on-chain. Votes are hidden during the voting period. The tally can be revealed when voting ends, but individual votes remain private.

    During voting, both the individual votes (hasVoted) and the running tallies (yesVotes, noVotes) are shielded. No one can see which way the vote is trending. After the deadline, getResults() casts the shielded tallies to public uint256 values so the outcome can be read.

    hashtag
    Sealed-bid auctions

    Bids are hidden until the auction closes. No bidder can see what others have bid, eliminating bid sniping and strategic underbidding.

    Each bid is stored as a suint256, and the highest bidder's address is stored as an saddress. During the auction, the contract tracks the leading bid internally, but no one outside the TEE can see any bid amounts or the current leader. After the auction ends, getWinner() reveals the result by casting shielded values to their public counterparts.

    Deploy an SRC20 in 60 Seconds

    Deploy a private SRC20 token on Seismic testnet with a single command — no compiler required

    The SRC20 Factory is a pre-deployed contract on Seismic testnet that lets anyone create a private token without installing sforge or writing a single line of Solidity. You give it a name, symbol, and supply; it hands back a deployed token address.

    hashtag
    Quickstart

    bunx create-src20

    That's it. The CLI walks you through the rest interactively, or you can pass everything up front:

    bunx create-src20 \
      --name "My Private Token" \
      --symbol "MPT" \
      --supply 1000000 \
      --key 0xYourPrivateKey

    Example output:

      Create SRC20 Token on Seismic
    
      Deploying to Seismic testnet...
    
      Token deployed!
    
      Address:  0xabc...
      Name:     My Private Token
      Symbol:   MPT
      Supply:   1,000,000
      Owner:    0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266
      Tx:       0xdef...
      Explorer: https://seismic-testnet.socialscan.io/address/0xabc...

    hashtag
    CLI flags

    Flag
    Description
    Default
    circle-info

    Supply is always treated as whole tokens. Passing --supply 1000000 mints 1,000,000 × 10¹⁸ base units, using 18 decimals.

    hashtag
    What you get

    Every deployed token is a full SRC20Token contract — Seismic's private variant of ERC20. Balances, transfer amounts, and allowances are encrypted on-chain using suint256 shielded types. Outside observers see only encrypted ciphertext. The token deployer becomes the owner and has exclusive mint and burn rights.

    hashtag
    Interfaces

    The factory exposes four interfaces depending on how you want to integrate:

    Interface
    When to use

    hashtag
    Network

    • Chain ID: 5124 (Seismic testnet)

    • Factory address: 0x87F850cbC2cFfac086F20d0d7307E12d06fA2127

    • Explorer:

    TypeScript SDK

    API reference for the @seismic/src20-sdk package used internally by the CLI and web app

    @seismic/src20-sdk is the TypeScript library that powers the CLI and web interface in this repo. It wraps the SRC20 Factory contract and exposes three functions and the contract ABIs.

    circle-info

    @seismic/src20-sdk is an internal workspace package — it is not published to npm. It is used by the packages/cli and packages/web packages within this monorepo. This page documents its API surface as a reference.


    hashtag
    createToken

    Deploys a new SRC20 token through the factory and returns the deployed address.

    hashtag
    Signature

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns


    hashtag
    getTokenInfo

    Reads public metadata from a deployed SRC20 token using a standard public client.

    hashtag
    Signature

    hashtag
    Returns


    hashtag
    getFactoryAddress

    Resolves the factory contract address for a given chain ID. Throws if the chain is not supported.

    hashtag
    Signature

    hashtag
    FACTORY_ADDRESSES

    The raw mapping of chain IDs to factory addresses:


    hashtag
    ABIs

    The package exports the full contract ABIs:

    • SRC20FactoryAbi — createToken, getTokenCount, tokens(uint256), TokenCreated event

    • SRC20TokenAbi — name

    Contracts

    Solidity interface reference for SRC20Factory and SRC20Token

    hashtag
    SRC20Factory

    Pre-deployed on Seismic testnet at 0x87F850cbC2cFfac086F20d0d7307E12d06fA2127.

    contract SRC20Factory {
        event TokenCreated(
            address indexed creator,
            address indexed token,
            string name,
            string symbol
        );
    
        address[] public tokens;
    
        function createToken(
            string memory name,
            string memory symbol,
            uint8 decimals,
            suint256 initialSupply
        ) external returns (address);
    
        function getTokenCount() external view returns (uint256);
    }

    hashtag
    createToken

    Deploys a new SRC20Token, records it in the tokens array, emits TokenCreated, and returns the deployed address. The caller becomes the token's owner.

    initialSupply is a suint256 — it is encrypted in the transaction and never visible on-chain. The supply is minted directly to msg.sender in the token's constructor.

    hashtag
    tokens

    The public tokens array stores every token address in deployment order. Access by index:

    hashtag
    getTokenCount

    Returns tokens.length. Use this to iterate the full list.

    hashtag
    TokenCreated event

    Emitted on every successful deployment. The name and symbol fields are unencrypted strings; creator and token are indexed for log filtering.


    hashtag
    SRC20Token

    Each token deployed by the factory is an instance of SRC20Token, which extends the base SRC20 contract from seismic-std-lib.

    hashtag
    mint / burn

    Only callable by owner. Both revert with "NOT_OWNER" if called by anyone else. The amount parameter is suint256 — it is shielded in transit.

    hashtag
    Inherited from SRC20

    Function
    Description

    balance() and allowance() use Seismic's signed-read mechanism — the caller must submit an off-chain signature that authorizes the node to decrypt and return their private state. See for details.

    hashtag
    Why not clones?

    The factory deploys a full SRC20Token contract for each token rather than using EIP-1167 minimal proxies. SRC20 stores decimals as an immutable field and computes the EIP-2612 domain separator in the constructor — both must be baked into bytecode at deployment time. Clones share bytecode and cannot carry per-instance immutables, so they are not compatible with SRC20.

    Clown Beatdown

    Imagine a clown standing in front of you, taunting the crowd. Hidden in the clown's pockets are secrets — strings that only become visible once the clown is knocked out. There are primarily two actions you can take: hitting the clown or adding secrets to the clown's pockets. Hitting the clown reduces its stamina by one, bringing it closer to being knocked out. Adding a secret stores an encrypted string that no one can read until it's revealed. You can only rob the clown of a secret if you contributed to knocking it out, i.e., you hit the clown at least once in the current round. This collaborative challenge is the heart of the Clown Beatdown game, and it's all powered by the ClownBeatdown smart contract. Let's dive into the inner workings of the contract and uncover how each part fuels this game.

    The contract can be found in the contracts/src/ClownBeatdown.sol file of the starter repo.

    hashtag
    State variables

    hashtag
    initialClownStamina and clownStamina

    Think of stamina as the clown's durability. initialClownStamina is the clown's starting strength, and clownStamina tracks how much remains as players hit it.

    hashtag
    secrets, secretsCount, and secretIndex

    These are the hidden values at the heart of the game. secrets is a mapping of sbytes (shielded bytes) — encrypted strings stored on-chain that remain hidden until revealed. secretsCount tracks how many secrets have been added. secretIndex is a suint256 (shielded integer) that determines which secret will be revealed when the clown is robbed. Because secretIndex is shielded, no one can predict which secret will be returned.

    hashtag
    round

    A counter that increments with each new round/reset, ensuring every round has a fresh clown to beat down.

    hashtag
    hitsPerRound

    A mapping that records every player's contribution to the current round, ensuring only participants can rob the clown's secret.

    hashtag
    Functions

    hashtag
    addSecret (string _secret)

    This function allows anyone to add a secret to the clown's pool. Since addSecret converts a plain string into sbytes (shielded bytes) and stores it in the secrets mapping, it performs a shielded write — the secret's contents are encrypted on-chain and invisible to observers.

    What happens:

    • Converts the input string to sbytes and stores it in the secrets mapping

    • Increments secretsCount

    • Re-picks a random secretIndex

    hashtag
    hit ( )

    This function allows a player to hit the clown, reducing its stamina and bringing it one step closer to being knocked out:

    What happens:

    • Checks if the clown is still standing (clownStamina > 0)

    • If it is, decrements clownStamina by 1

    • Increases the player who called hit()'s contribution in the current round (hitsPerRound[round][playerAddress]

    hashtag
    rob ( )

    This function allows contributors to the current round to steal a randomly selected secret from the clown. Since this is a view function that reveals an sbytes value, calling this function constitutes a signed read.

    What happens:

    • Requires the clown to be knocked out (requireDown modifier)

    • Ensures the function caller contributed to knocking out the clown for this round (onlyContributor modifier)

    • Returns the secret at the shielded secretIndex position, decrypted from

    hashtag
    reset ( )

    This function resets the clown for a new round of gameplay:

    What happens:

    • Requires the clown to be knocked out (requireDown modifier)

    • Restores clownStamina to initialClownStamina

    • Picks a new random secretIndex

    hashtag
    Modifiers

    Modifiers enforce the rules of the game:

    hashtag
    requireDown

    Ensures that rob() and reset() can only be called if the clown's stamina has reached zero.

    hashtag
    requireStanding

    Ensures that hit() can only be called while the clown still has stamina remaining.

    hashtag
    onlyContributor

    Restricts access to rob(), and hence the secret being revealed, only to players who contributed at least one hit in the current round.

    Ch 1: Constants and Utilities

    In this chapter, you will learn about defining constants and utility functions which we will frequently use throughout the project. Estimated time: ~10 minutes

    First, navigate to the root of the directory/monorepo and run bun install to install all the dependencies.

    Now, navigate to make a lib folder inside packages/cli with the files constants.ts and utils.ts and navigate to it:

    mkdir -p packages/cli/lib
    touch packages/cli/lib/constants.ts packages/cli/lib/utils.ts
    cd packages/cli/lib

    constants.ts will contain the constants we use throughout the project while utils.ts will contain the necessary utility functions.

    Add the following to constants.ts:

    This file centralizes key project constants:

    • CONTRACT_NAME: The ClownBeatdown contract name.

    • CONTRACT_DIR: Path to the contracts directory.

    Now, add the following to utils.ts:

    This file contains utility functions to interact with your ClownBeatdown contract:

    • getShieldedContractWithCheck: Ensures the contract is deployed and returns a shielded contract instance.

    • readContractAddress: Reads the deployed contract's address from a broadcast file.

    • readContractABI: Parses the contract's ABI from an ABI file for use in interactions.

    Shielded Types

    A handle on stype unlocks all shielded computation and storage

    Operations on shielded types return shielded types. For example, comparing two suint256 values produces an sbool, not a bool. Arithmetic on sint256 returns sint256, and so on.

    hashtag
    Shielded Integers

    All comparisons and operators for shielded integers are functionally identical to their unshielded counterparts.

    hashtag
    suint - Shielded Unsigned Integer

    hashtag
    sint - Shielded Signed Integer

    hashtag
    sbool - Shielded Boolean

    All comparisons and operators for sbool function identically to bool.

    We recommend reading the point on prior to using sbool since it's easy to accidentally leak information with this type.

    hashtag
    saddress - Shielded Address

    An saddress variable supports code and codehash members only. Members like call, delegatecall, staticcall, balance, and transfer are not available — you must cast to address first.

    hashtag
    sbytes - Shielded Bytes

    hashtag
    Fixed-size: sbytes1 through sbytes32

    Fixed-size shielded bytes mirror the standard bytes1–bytes32 types.

    hashtag
    Dynamic: sbytes

    Dynamic shielded bytes mirror the standard bytes type. The length is stored as shielded — like , observers cannot read it directly but may infer an upper bound from gas costs.

    hashtag
    Shielded Literals

    You can create shielded integer constants with either an explicit cast or the s suffix:

    The s suffix infers the shielded type from context and works with hex, underscores, scientific notation, and unary minus. See for the full specification.

    circle-exclamation

    Both forms embed the literal value in the contract bytecode, which is publicly visible — the initial value is leaked at deployment time. The compiler emits warning 9660 to remind you.

    This is fine for values meant to be public initially and then evolve through private state changes. But if the literal itself is sensitive, do not hardcode it. See for more detail.

    Sanvil

    Local development chain configurations

    Chain configuration for connecting to a locally-running Seismic Anvil (Sanvil) instance. sanvil is a RainbowKit-compatible chain object pre-configured for the default Sanvil endpoint.

    hashtag
    Configuration

    Property
    Value

    hashtag
    Import

    hashtag
    Usage

    hashtag
    With RainbowKit

    hashtag
    With wagmi Config

    hashtag
    localSeismicDevnet

    localSeismicDevnet is a separate chain configuration for connecting to a locally-running seismic-reth node started in --dev mode. Use this instead of sanvil when you are running a full seismic-reth node locally rather than a Sanvil instance.

    circle-info

    Use sanvil for Sanvil (Seismic Anvil) instances. Use localSeismicDevnet for seismic-reth nodes running with the --dev flag.

    hashtag
    Installing Sanvil

    Sanvil is part of the Seismic Foundry toolchain. Install it with sfoundryup:

    Then start a local node:

    By default, Sanvil:

    • Listens on 127.0.0.1:8545

    • Uses chain ID 31337

    • Pre-funds test accounts with ETH

    hashtag
    See Also

    • - All supported chains

    • - Public testnet configuration

    • - Custom chain factory

    Ch 2: Core App Logic

    In this chapter, you'll write the core logic to interact with the ClownBeatdown contract by creating an App class. This class will initialize player-specific wallet clients and contracts, and provide easy-to-use functions like hit, rob, and reset. Estimated time: ~20 minutes

    Now, navigate to packages/cli/src/ and create a file called app.ts which will contain the core logic for the CLI:

    hashtag
    Import required dependencies

    useShieldedWallet

    Access shielded public and wallet clients from context

    Hook that consumes the ShieldedWalletProvider context. Returns the shielded public client, wallet client, connected address, error state, and a loaded flag indicating whether initialization is complete.

    hashtag
    Return Type

    Property
    Type

    Hooks

    React hooks for shielded transactions and signed reads

    seismic-react provides four hooks that mirror wagmi's hook API but route through Seismic's shielded transport layer. Each hook wraps the corresponding seismic-viem client or contract method, handling encryption, signing, and decryption automatically.

    hashtag
    Hook Comparison

    seismic-react Hook
    wagmi Equivalent
    Purpose

    Chains

    RainbowKit-compatible chain configurations for Seismic networks

    seismic-react provides pre-configured chain objects compatible with RainbowKit's getDefaultConfig. Each chain wraps a chain definition with RainbowKit metadata (icon, etc.).

    hashtag
    Import

    Wallet Guides

    Integrate Seismic with popular wallet connection libraries

    seismic-react integrates with any wallet library that provides wagmi configuration. This section covers setup for the most popular options.

    hashtag
    Supported Libraries

    Library
    Best For
    Key Feature

    PublicContract

    Sync contract wrapper with transparent read-only access

    Synchronous contract wrapper for read-only transparent access to Seismic contracts.

    hashtag
    Overview

    PublicContract provides a simplified interface for reading public contract state without requiring encryption or a private key. It exposes only the .tread namespace for standard eth_call operations. Use this class when you only need to read public contract data and don't need shielded operations.

    Create Seismic Devnet

    Factory function for custom Seismic chain configurations

    Factory function that creates a RainbowKit-compatible chain configuration for any Seismic node. Use this when the pre-configured chains (seismicTestnet, sanvil, localSeismicDevnet) do not match your node's host.

    hashtag
    Import

    using
    _randomIndex()
    ) by 1
  • Emits the Hit event to update all participants.

  • sbytes
    to
    bytes
    .
  • Increments the round counter

  • Emits the Reset event.

  • Native Currency

    ETH (18 decimals)

    Wallet Guides
    Basic dApp
    AES-GCM Decrypt 0x67 — decrypt data with AES-GCM
  • HKDF 0x68 — derive cryptographic keys from a parent key

  • secp256k1 Sign 0x69 — sign a message given a secret key

  • — returns zero for shielded slots, making them indistinguishable from uninitialized storage
  • Tracing endpoints are currently disabled. We plan to re-enable them with private data redacted from traces

  • Almost all
    minor exceptions
    CLOAD
    CSTORE
    TIMESTAMP_MS
    precompiles
    RNG 0x64
    ECDH 0x65
    AES-GCM Encrypt 0x66
    RPC Methods
    seismic_getTeePublicKey
    eth_call
    "signed reads"
    eth_sendRawTransaction
    eth_getStorageAt

    Building the CLI

    In this section, you will write a CLI to interact with the deployed ClownBeatdown smart contract from scratch, simulating a multiplayer, multi-round game consisting of two players, Alice and Bob. At the end of this section, you will be able to see a full game play out on your local Seismic node! Estimated time: ~30 minutes

    SRC20 tutorial

    Token name

    params.symbol

    string

    yes

    Token symbol

    params.initialSupply

    bigint

    yes

    Supply in base units (e.g. 1_000_000n * 10n ** 18n)

    params.decimals

    number

    no

    Token decimals, defaults to 18

    ,
    symbol
    ,
    decimals
    ,
    owner
    ,
    totalSupply
    ,
    mint
    ,
    burn
    , and all inherited SRC20 functions (
    transfer
    ,
    transferFrom
    ,
    approve
    ,
    balance
    ,
    allowance
    ,
    balanceOfSigned
    ,
    permit
    )

    client

    ShieldedWalletClient

    yes

    A shielded wallet client from seismic-viem

    params.name

    string

    yes

    Caller's own encrypted balance (requires a signed read)

    allowance(address spender) → uint256

    Caller's allowance for spender (requires a signed read)

    balanceOfSigned(address owner, uint256 expiry, bytes signature) → uint256

    Signed read pattern for balance

    permit(...)

    EIP-2612 permit with encrypted amounts

    transfer(address to, suint256 amount) → bool

    Encrypted transfer to to

    transferFrom(address from, address to, suint256 amount) → bool

    Encrypted transferFrom

    approve(address spender, suint256 amount) → bool

    Set encrypted allowance

    Signed Reads

    balance() → uint256

    import { join } from "path";
    
    const CONTRACT_NAME = "ClownBeatdown";
    const CONTRACT_DIR = join(__dirname, "../../contracts");
    
    export { CONTRACT_NAME, CONTRACT_DIR };
    conditional execution
    dynamic shielded arrays
    Shielded Literals
    Footguns
    Provides instant block mining
    - RainbowKit setup guides

    Chain ID

    31337

    Name

    Sanvil

    RPC (HTTP)

    http://127.0.0.1:8545

    Native Currency

    ETH (18 decimals)

    Chains Overview
    Seismic Testnet
    createSeismicDevnet
    Wallet Guides
    Start by importing all the necessary modules and functions at the top of app.ts:

    hashtag
    Define the app configuration

    The AppConfig interface organizes all settings for the Clown Beatdown app, including player info, wallet setup, and contract details. It supports a multiplayer environment, with multiple players having distinct private keys and contract interactions.

    hashtag
    Create the App class

    The App class manages player-specific wallet clients and contract instances, providing an easy-to-use interface for multiplayer gameplay.

    hashtag
    Add initialization logic to App

    The init() method sets up individual wallet clients and contract instances for each player, enabling multiplayer interactions. Each player gets their own wallet client and a direct connection to the contract.

    hashtag
    Add helper methods to App

    These helper methods ensure that the app fetches the correct contract instance for a specific player, supporting multiplayer scenarios.

    getPlayerContract:

    hashtag
    Implement Contract Interaction Methods

    reset

    Resets the clown for the next round. The reset restores stamina and picks a new random secret.

    hit

    A player can hit the clown to reduce its stamina. Each hit is logged for the respective player.

    rob

    Reveals a secret for a specific player if they contributed to knocking out the clown. This ensures fairness in multiplayer gameplay. Uses signed reads.

    suint256 reserve0;
    suint256 reserve1;
    mapping(address => suint256) liquidity;
    
    function swap(address tokenIn, suint256 amountIn) internal returns (suint256 amountOut) {
        if (tokenIn == token0) {
            amountOut = (amountIn * reserve1) / (reserve0 + amountIn);
            reserve0 += amountIn;
            reserve1 -= amountOut;
        } else {
            amountOut = (amountIn * reserve0) / (reserve1 + amountIn);
            reserve1 += amountIn;
            reserve0 -= amountOut;
        }
    }
    import "@openzeppelin/contracts/access/AccessControl.sol";
    
    bytes32 public constant COMPLIANCE_ROLE = keccak256("COMPLIANCE_ROLE");
    
    mapping(address => suint256) balanceOf;
    
    function getBalance(address account) public view returns (uint256) {
        require(
            msg.sender == account || hasRole(COMPLIANCE_ROLE, msg.sender),
            "Not authorized"
        );
        return uint256(balanceOf[account]);
    }
    
    function transfer(address to, suint256 amount) public {
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }
    mapping(address => sbool) hasVoted;
    suint256 yesVotes;
    suint256 noVotes;
    uint256 public votingEnd;
    
    function vote(sbool inFavor) public {
        require(block.timestamp < votingEnd, "Voting ended");
        require(!bool(hasVoted[msg.sender]), "Already voted");
    
        hasVoted[msg.sender] = sbool(true);
        if (bool(inFavor)) {
            yesVotes += 1s;
        } else {
            noVotes += 1s;
        }
    }
    
    function getResults() public view returns (uint256 yes, uint256 no) {
        require(block.timestamp >= votingEnd, "Voting still open");
        yes = uint256(yesVotes);
        no = uint256(noVotes);
    }
    mapping(address => suint256) bids;
    suint256 highestBid;
    saddress highestBidder;
    uint256 public auctionEnd;
    
    function bid(suint256 amount) public {
        require(block.timestamp < auctionEnd, "Auction ended");
        bids[msg.sender] = amount;
    
        if (uint256(amount) > uint256(highestBid)) {
            highestBid = amount;
            highestBidder = saddress(msg.sender);
        }
    }
    
    function getWinner() public view returns (address winner, uint256 amount) {
        require(block.timestamp >= auctionEnd, "Auction still open");
        winner = address(highestBidder);
        amount = uint256(highestBid);
    }
    async function createToken(
      client: ShieldedWalletClient,
      params: CreateTokenParams,
    ): Promise<CreateTokenResult>;
    interface CreateTokenResult {
      tokenAddress: Address; // address of the deployed SRC20Token contract
      txHash: Hash; // transaction hash
    }
    async function getTokenInfo(
      client: PublicClient,
      tokenAddress: Address,
    ): Promise<TokenInfo>;
    interface TokenInfo {
      name: string;
      symbol: string;
      decimals: number;
      owner: Address;
      totalSupply: bigint; // in base units
    }
    function getFactoryAddress(chainId: number): Address;
    const FACTORY_ADDRESSES: Record<number, Address> = {
      5124: "0x87F850cbC2cFfac086F20d0d7307E12d06fA2127",
    };
    address token = factory.tokens(0);
    contract SRC20Token is SRC20 {
        address public owner;
    
        constructor(
            string memory _name,
            string memory _symbol,
            uint8 _decimals,
            suint256 _initialSupply,
            address _owner
        );
    
        function totalSupply() public view returns (uint256);
        function mint(address to, suint256 amount) external;   // owner only
        function burn(address from, suint256 amount) external; // owner only
    }
    import fs from "fs";
    import { type ShieldedWalletClient, getShieldedContract } from "seismic-viem";
    import { Abi, Address } from "viem";
    
    async function getShieldedContractWithCheck(
      walletClient: ShieldedWalletClient,
      abi: Abi,
      address: Address,
    ) {
      const contract = getShieldedContract({
        abi: abi,
        address: address,
        client: walletClient,
      });
    
      const code = await walletClient.getCode({
        address: address,
      });
      if (!code) {
        throw new Error("Please deploy contract before running this script.");
      }
    
      return contract;
    }
    
    function readContractAddress(broadcastFile: string): `0x${string}` {
      const broadcast = JSON.parse(fs.readFileSync(broadcastFile, "utf8"));
      if (!broadcast.transactions?.[0]?.contractAddress) {
        throw new Error("Invalid broadcast file format");
      }
      return broadcast.transactions[0].contractAddress;
    }
    
    function readContractABI(abiFile: string): Abi {
      const abi = JSON.parse(fs.readFileSync(abiFile, "utf8"));
      if (!abi.abi) {
        throw new Error("Invalid ABI file format");
      }
      return abi.abi;
    }
    
    export { getShieldedContractWithCheck, readContractAddress, readContractABI };
    suint256 a = suint256(10);
    suint256 b = suint256(3);
    
    // == EXAMPLES
    a > b   // sbool(true)
    a | b   // suint256(11)
    a << 2  // suint256(40)
    a % b   // suint256(1)
    sint256 a = sint256(-10);
    sint256 b = sint256(3);
    
    // == EXAMPLES
    a < b   // sbool(true)
    a + b   // sint256(-7)
    a * b   // sint256(-30)
    sbool a = sbool(true);
    sbool b = sbool(false);
    
    // == EXAMPLES
    a && b  // sbool(false)
    !b      // sbool(true)
    saddress a = saddress(0x123);
    saddress b = saddress(0x456);
    
    // == VALID EXAMPLES
    a == b  // sbool(false)
    b.code
    b.codehash
    
    // == INVALID EXAMPLES
    a.balance   // must cast to address first
    a.call("")  // must cast to address first
    sbytes32 a = sbytes32(0xabcd);
    sbytes1 b = sbytes1(0xff);
    suint256 a = suint256(42);   // explicit cast
    suint256 b = 42s;            // s suffix — same result
    import { sanvil } from "seismic-react/rainbowkit";
    import { getDefaultConfig } from "@rainbow-me/rainbowkit";
    import { sanvil } from "seismic-react/rainbowkit";
    
    const config = getDefaultConfig({
      appName: "My App",
      projectId: "YOUR_PROJECT_ID",
      chains: [sanvil],
    });
    import { http, createConfig } from "wagmi";
    import { sanvil } from "seismic-react/rainbowkit";
    
    const config = createConfig({
      chains: [sanvil],
      transports: {
        [sanvil.id]: http(),
      },
    });
    import { localSeismicDevnet } from "seismic-react/rainbowkit";
    
    const config = getDefaultConfig({
      appName: "My App",
      projectId: "YOUR_PROJECT_ID",
      chains: [localSeismicDevnet],
    });
    curl -L https://raw.githubusercontent.com/SeismicSystems/seismic-foundry/seismic/sfoundryup/install | bash
    sfoundryup
    sanvil
    # Assuming you are in packages/cli/lib
    cd ../src
    touch app.ts
    import {
      type ShieldedContract,
      type ShieldedWalletClient,
      createShieldedWalletClient,
    } from "seismic-viem";
    import { Abi, Address, Chain, http, hexToString } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    
    import { getShieldedContractWithCheck } from "../lib/utils";
    interface AppConfig {
      players: Array<{
        name: string; // Name of the player
        privateKey: string; // Private key for the player's wallet
      }>;
      wallet: {
        chain: Chain; // Blockchain network (e.g., Seismic Testnet or sanvil)
        rpcUrl: string; // RPC URL for blockchain communication
      };
      contract: {
        abi: Abi; // The contract's ABI for interaction
        address: Address; // The contract's deployed address
      };
    }
    export class App {
      private config: AppConfig; // Holds all app configuration
      private playerClients: Map<string, ShieldedWalletClient> = new Map(); // Maps player names to their wallet clients
      private playerContracts: Map<string, ShieldedContract> = new Map(); // Maps player names to their contract instances
    
      constructor(config: AppConfig) {
        this.config = config;
      }
    }
    async init() {
      for (const player of this.config.players) {
        // Create a wallet client for the player
        const walletClient = await createShieldedWalletClient({
          chain: this.config.wallet.chain,
          transport: http(this.config.wallet.rpcUrl),
          account: privateKeyToAccount(player.privateKey as `0x${string}`),
        })
        this.playerClients.set(player.name, walletClient) // Map the client to the player
    
        // Initialize the player's contract instance and ensure the contract is deployed
        const contract = await getShieldedContractWithCheck(
          walletClient,
          this.config.contract.abi,
          this.config.contract.address
        )
        this.playerContracts.set(player.name, contract) // Map the contract to the player
      }
    }
    private getPlayerContract(playerName: string): ShieldedContract {
      const contract = this.playerContracts.get(playerName)
      if (!contract) {
        throw new Error(`Shielded contract for player ${playerName} not found`)
      }
      return contract
    }
    async reset(playerName: string) {
      console.log(`- Player ${playerName} writing reset()`)
      const contract = this.getPlayerContract(playerName)
      await contract.write.reset([])
    }
    async hit(playerName: string) {
      console.log(`- Player ${playerName} writing hit()`)
      const contract = this.getPlayerContract(playerName)
      await contract.write.hit([])
    }
    async rob(playerName: string) {
      console.log(`- Player ${playerName} reading rob()`)
      const contract = this.getPlayerContract(playerName)
      const result = await contract.read.rob() // signed read
      const decoded = hexToString(result as `0x${string}`)
      console.log(`- Player ${playerName} robbed secret:`, decoded)
    }

    Initial supply in whole tokens (multiplied by 10¹⁸ internally)

    prompted

    --key

    0x-prefixed 64-character hex private key

    prompted (hidden input)

    --rpc

    Custom RPC URL

    https://testnet-2.seismictest.net/rpc

    Query deployed tokens from any language

    --name

    Token name

    prompted

    --symbol

    Token symbol

    prompted

    CLI

    Deploy a token manually from the terminal

    TypeScript SDK

    Embed token creation in a dApp or script

    Web GUI

    Deploy via browser without writing code

    seismic-testnet.socialscan.ioarrow-up-right

    --supply

    Description

    publicClient

    ShieldedPublicClient | null

    Shielded public client for reads

    walletClient

    ShieldedWalletClient | null

    Shielded wallet client for writes

    address

    Hex | null

    Connected wallet address

    error


    hashtag
    Usage

    hashtag
    Basic

    hashtag
    Handling loading state

    The loaded flag is false until the shielded clients have been fully initialized. Use it to avoid rendering components that depend on the wallet client before it is ready.

    hashtag
    Error handling

    If initialization fails (for example, the node is unreachable or the wallet connector is incompatible), the error field contains the error message.

    hashtag
    Accessing clients for direct seismic-viem calls

    The returned publicClient and walletClient are full seismic-viem client instances. You can use them directly for operations not covered by the higher-level hooks.

    circle-exclamation

    This hook must be used within a ShieldedWalletProvider. It will throw if called outside the provider tree.

    hashtag
    See Also

    • ShieldedWalletProvider -- Context provider that supplies the shielded clients

    • useShieldedContract -- Create a contract instance using the wallet client

    • useShieldedWriteContract -- Send encrypted write transactions

    • -- Perform signed, encrypted read calls

    • -- Summary of all hooks

    import { useShieldedWallet } from "seismic-react";
    import { useShieldedWallet } from 'seismic-react'
    
    function WalletInfo() {
      const { address, walletClient, publicClient } = useShieldedWallet()
    
      if (!walletClient) return <div>Connect your wallet</div>
    
      return <div>Connected: {address}</div>
    }
    import { useShieldedWallet } from 'seismic-react'
    
    function App() {
      const { loaded, walletClient, address } = useShieldedWallet()
    
      if (!loaded) {
        return <div>Initializing shielded wallet...</div>
      }
    
      if (!walletClient) {
        return <div>Please connect your wallet</div>
      }
    
      return <div>Wallet ready: {address}</div>
    }
    import { useShieldedWallet } from 'seismic-react'
    
    function WalletStatus() {
      const { loaded, error, walletClient } = useShieldedWallet()
    
      if (!loaded) return <div>Loading...</div>
      if (error) return <div>Error: {error}</div>
      if (!walletClient) return <div>Not connected</div>
    
      return <div>Wallet connected</div>
    }
    import { useShieldedWallet } from 'seismic-react'
    
    function DirectClientUsage() {
      const { publicClient, walletClient } = useShieldedWallet()
    
      async function getBlockNumber() {
        if (!publicClient) return
        const block = await publicClient.getBlockNumber()
        console.log('Current block:', block)
      }
    
      async function sendRawTransaction() {
        if (!walletClient) return
        const hash = await walletClient.sendTransaction({
          to: '0x...',
          value: 0n,
        })
        console.log('Transaction hash:', hash)
      }
    
      return (
        <div>
          <button onClick={getBlockNumber}>Get Block</button>
          <button onClick={sendRawTransaction}>Send Tx</button>
        </div>
      )
    }

    useShieldedWallet

    useAccount + useWalletClient

    Access shielded public and wallet clients

    useShieldedContract

    useContract

    Get a ShieldedContract instance

    useShieldedWriteContract

    useWriteContract

    Send encrypted write transactions

    useSignedReadContract

    circle-info

    All hooks require ShieldedWalletProvider context. Calling any hook outside the provider tree will throw an error.

    hashtag
    Common Patterns

    hashtag
    Wallet check

    Always verify the wallet is loaded before calling contract methods:

    hashtag
    Loading states

    The write and read hooks expose isLoading so you can disable buttons or show spinners:

    hashtag
    Error handling

    Every hook returns an error field. Check it after operations complete:

    hashtag
    Pages

    Page
    Description

    Access shielded wallet and public clients from context

    Get a ShieldedContract instance for reads and writes

    Send encrypted write transactions

    hashtag
    See Also

    • ShieldedWalletProvider -- Context provider required by all hooks

    • Installation -- Package setup and peer dependencies

    • Seismic React Overview -- SDK architecture and quick start

    const { walletClient, loaded } = useShieldedWallet()
    
    if (!loaded) return <div>Loading wallet...</div>
    if (!walletClient) return <div>Please connect your wallet</div>
    const { writeContract, isLoading } = useShieldedWriteContract({ address, abi, functionName: 'transfer', args })
    
    return (
      <button onClick={writeContract} disabled={isLoading}>
        {isLoading ? 'Sending...' : 'Transfer'}
      </button>
    )
    const { signedRead, error } = useSignedReadContract({
      address,
      abi,
      functionName: "balanceOf",
    });
    
    async function fetchBalance() {
      const result = await signedRead();
      if (error) {
        console.error("Read failed:", error.message);
      }
    }
    hashtag
    Available Chains
    Chain
    Export
    Chain ID
    Description

    seismicTestnet

    5124

    Public testnet

    sanvil

    hashtag
    Usage with RainbowKit

    hashtag
    Choosing a Chain

    hashtag
    Relationship to seismic-viem Chains

    Each chain export is a thin wrapper around the corresponding seismic-viem chain object. The wrapper adds RainbowKit-specific metadata (such as iconUrl) while preserving all underlying chain properties -- chain ID, RPC URLs, native currency, block explorers, and transaction formatters.

    circle-info

    If you are not using RainbowKit, you can use the seismic-viem chain objects directly with wagmi.

    hashtag
    Pages

    Page
    Description

    Public testnet configuration and usage

    Local development chains (Sanvil + seismic-reth)

    Factory for custom chain configurations

    hashtag
    See Also

    • ShieldedWalletProvider - Provider that accepts chain config

    • Wallet Guides - RainbowKit and wallet setup guides

    • Installation - Package setup

    import {
      seismicTestnet,
      sanvil,
      localSeismicDevnet,
      createSeismicDevnet,
    } from "seismic-react/rainbowkit";
    seismic-viem
    import { getDefaultConfig } from "@rainbow-me/rainbowkit";
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    const config = getDefaultConfig({
      appName: "My App",
      projectId: "YOUR_PROJECT_ID",
      chains: [seismicTestnet],
    });
    Are you deploying to the public testnet?
      -> Use seismicTestnet
    
    Are you developing locally with Sanvil?
      -> Use sanvil
    
    Are you running a local seismic-reth node in --dev mode?
      -> Use localSeismicDevnet
    
    Are you connecting to a custom or self-hosted Seismic node?
      -> Use createSeismicDevnet()

    dApps wanting polished wallet UI

    Built-in modal, chain switching, account display

    Apps needing email/social login

    Embedded wallets, onboarding flow

    WalletConnect ecosystem apps

    WalletConnect modal, broad wallet support

    hashtag
    Common Integration Pattern

    Regardless of which wallet library you choose, the integration follows the same steps:

    1. Install the wallet library alongside seismic-react

    2. Configure wagmi with Seismic chain definitions

    3. Nest providers in the correct order

    4. Use hooks from seismic-react in your components

    hashtag
    Provider Nesting Order

    All wallet integrations require the same provider hierarchy:

    circle-exclamation

    ShieldedWalletProvider must be nested inside both WagmiProvider and your wallet provider. It reads the connected wallet from wagmi's context, so placing it outside will cause a runtime error.

    hashtag
    Chain Configuration

    All wallet libraries use the same chain imports from seismic-react/rainbowkit:

    hashtag
    Choosing a Library

    • Want the easiest setup with a beautiful wallet modal? Use RainbowKit

    • Need email/social login or embedded wallets? Use Privy

    • Want WalletConnect with maximum wallet compatibility? Use AppKit

    hashtag
    Pages

    Page
    Description

    Setup with RainbowKit wallet UI

    Embedded wallets with Privy

    WalletConnect AppKit integration

    hashtag
    See Also

    • Installation -- Package setup and peer dependencies

    • ShieldedWalletProvider -- Provider reference and configuration

    • Hooks Overview -- All available hooks

    WagmiProvider
      └─ QueryClientProvider
           └─ [Wallet Provider] (RainbowKit / Privy / AppKit)
                └─ ShieldedWalletProvider
                     └─ Your App
    import { seismicTestnet } from "seismic-react/rainbowkit";

    hashtag
    Definition

    hashtag
    Constructor Parameters

    Parameter
    Type
    Required
    Description

    w3

    Web3

    Yes

    Synchronous Web3 instance connected to RPC endpoint

    address

    ChecksumAddress

    hashtag
    Namespace

    hashtag
    .tread - Transparent Read

    Executes standard eth_call with unencrypted calldata. This is the only namespace available on PublicContract.

    Returns: Any (ABI-decoded Python value)

    Optional Parameters: None (pass positional arguments only)

    hashtag
    Examples

    hashtag
    Basic Read Operations

    hashtag
    Single and Multiple Returns

    hashtag
    Array Results

    hashtag
    Error Handling

    hashtag
    Notes

    • Read-only: No write operations available (no .write, .twrite, or .dwrite namespaces)

    • No encryption required: Does not use EncryptionState or private keys

    • No authentication: Standard unsigned eth_call operations

    • Gas not consumed: eth_call is free (doesn't create transactions)

    • Public data only: Cannot access shielded/encrypted contract state

    hashtag
    See Also

    • AsyncPublicContract - Async version of this class

    • ShieldedContract - Full contract wrapper with write operations

    • create_public_client - Create client without private key

    • - Overview of contract interaction patterns

    class PublicContract:
        def __init__(
            self,
            w3: Web3,
            address: ChecksumAddress,
            abi: list[dict[str, Any]],
        ) -> None:
            ...
    from seismic_web3 import create_public_client, PublicContract
    
    # Create client without private key
    w3 = create_public_client(
        rpc_url="https://testnet-1.seismictest.net/rpc",
    )
    
    # Create read-only contract instance
    contract = PublicContract(
        w3=w3,
        address="0x1234567890123456789012345678901234567890",
        abi=CONTRACT_ABI,
    )
    
    # Read public contract state (auto-decoded)
    total_supply = contract.tread.totalSupply()  # int
    print(f"Total supply: {total_supply}")
    
    balance = contract.tread.balanceOf("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")  # int
    print(f"Balance: {balance}")
    # Single return values are returned directly
    number = contract.tread.getNumber()      # int
    name = contract.tread.getName()          # str
    is_active = contract.tread.isActive()    # bool
    
    # Multiple outputs are returned as a tuple
    user_name, user_balance, active = contract.tread.getUserInfo(
        "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    )
    # Read array of addresses (auto-decoded to list)
    holders = contract.tread.getHolders()
    print(f"Found {len(holders)} holders")
    for holder in holders:
        print(f"  - {holder}")
    try:
        value = contract.tread.getNumber()
        print(f"Value: {value}")
    
    except ValueError as e:
        print(f"RPC error: {e}")
    except Exception as e:
        print(f"Unexpected error: {e}")
    hashtag
    Parameters
    Parameter
    Type
    Required
    Description

    nodeHost

    string

    Yes

    Hostname for the node (e.g. testnet-1.seismictest.net)

    explorerUrl

    string

    hashtag
    Return Type

    RainbowKitChain -- a chain object compatible with RainbowKit's getDefaultConfig and wagmi's createConfig.

    The returned chain has:

    • Chain ID: 5124

    • Name: Seismic

    • Native Currency: ETH (18 decimals)

    • RPC (HTTPS): https://<nodeHost>/rpc

    • RPC (WSS): wss://<nodeHost>/ws

    • Seismic transaction formatters

    hashtag
    Usage

    hashtag
    Basic

    hashtag
    With RainbowKit

    hashtag
    With wagmi Config

    hashtag
    With Explorer URL

    hashtag
    Notes

    • The nodeHost parameter should be the bare hostname without a protocol prefix or path. HTTPS and WSS URLs are constructed automatically.

    • The Seismic icon is included automatically for display in RainbowKit's chain selector.

    • The underlying implementation delegates to createSeismicDevnet from seismic-viem and wraps the result with RainbowKit metadata.

    hashtag
    See Also

    • Chains Overview - All supported chains

    • Seismic Testnet - Public testnet configuration

    • Sanvil - Local development chains

    • - RainbowKit setup guides

    import { createSeismicDevnet } from "seismic-react/rainbowkit";
    import { createSeismicDevnet } from "seismic-react/rainbowkit";
    
    const myDevnet = createSeismicDevnet({
      nodeHost: "my-node.example.com",
    });
    // RPC: https://my-node.example.com/rpc
    // WSS: wss://my-node.example.com/ws
    import { getDefaultConfig } from "@rainbow-me/rainbowkit";
    import { createSeismicDevnet } from "seismic-react/rainbowkit";
    
    const myChain = createSeismicDevnet({
      nodeHost: "my-node.example.com",
    });
    
    const config = getDefaultConfig({
      appName: "My App",
      projectId: "YOUR_PROJECT_ID",
      chains: [myChain],
    });
    import { http, createConfig } from "wagmi";
    import { createSeismicDevnet } from "seismic-react/rainbowkit";
    
    const myChain = createSeismicDevnet({
      nodeHost: "my-node.example.com",
    });
    
    const config = createConfig({
      chains: [myChain],
      transports: {
        [myChain.id]: http(),
      },
    });
    const myChain = createSeismicDevnet({
      nodeHost: "my-node.example.com",
      explorerUrl: "https://explorer.example.com",
    });

    Ch 4: Testing

    In this chapter, you'll write tests to verify that the ClownBeatdown contract behaves as expected under various scenarios. Testing ensures the functionality, fairness, and access control mechanisms of your contract work seamlessly, particularly in multi-round gameplay. Estimated Time: ~15 minutes.

    hashtag
    Getting Started

    Navigate to the test folder in your project and open the ClownBeatdown.t.sol file located at:

    packages/contracts/test/ClownBeatdown.t.sol

    This file is where you'll write all the test cases for the ClownBeatdown contract. Start with the following base code:

    // SPDX-License-Identifier: MIT License
    pragma solidity ^0.8.13;
    
    import {Test} from "forge-std/Test.sol";
    import {ClownBeatdown} from "../src/ClownBeatdown.sol";
    
    contract ClownBeatdownTest is Test {
        ClownBeatdown public clownBeatdown;
    
        function setUp() public {
            clownBeatdown = new ClownBeatdown(2);
            clownBeatdown.addSecret("Secret A");
            clownBeatdown.addSecret("Secret B");
            clownBeatdown.addSecret("Secret C");
        }
    }

    The setUp() function initializes the ClownBeatdown contract with a stamina of 2 and adds three secrets to the pool.

    hashtag
    Writing Test Cases

    Start off with testing the basic functionalities: hit, rob, and reset.

    hashtag
    Core functionalities

    1. Basic hit functionality

    Ensures the clown's stamina decreases when hit.

    1. Knockout and rob

    Validates that after knocking out the clown, a contributor can rob a valid secret.

    1. Reset functionality

    1. Secret can change after reset

    Validates that secrets returned across rounds are always valid (they may or may not differ depending on randomness).

    Now, test for the restrictive/conditional nature of these basic functionalities.

    hashtag
    Restricting Actions

    1. Preventing hit when clown is down

    Ensures that hitting a knocked-out clown is not allowed.

    1. Preventing rob when clown is standing

    Ensures that robbing while the clown still has stamina is not allowed.

    1. Preventing reset when clown is standing

    Validates that the clown cannot be reset unless it is fully knocked out.

    Now, test for more complex scenarios.

    hashtag
    Complex scenarios

    1. Prevent Non-Contributors From Using rob()

    Ensures that only contributors in the current round can call rob().

    1. Contributor Tracking Across Rounds

    Validates that contributions are tracked independently for each round. The test has one contributor knock out the clown in round 1, and a different contributor knock it out in round 2. We check that the round 2 contributor can rob while the round 1 contributor cannot.

    Test out the file by running the following inside the packages/contracts directory:

    The contract has been tested, time to deploy it!

    Casting

    hashtag
    Explicit Casting Only

    Shielded types and their unshielded counterparts do not support implicit casting. You must always cast explicitly. This is a deliberate design decision -- every conversion between shielded and unshielded types is a potential information boundary, and the compiler requires you to be explicit about crossing it.

    uint256 publicNumber = 100;
    
    // Implicit casting -- will NOT compile
    suint256 shielded = publicNumber; // Error
    
    // Explicit casting -- correct
    suint256 shielded = suint256(publicNumber); // OK

    This applies to all shielded types: suint, sint, sbool, saddress, and sbytes.

    circle-info

    For integer literals specifically, you can use the instead of an explicit cast: suint256 x = 42s; is equivalent to suint256 x = suint256(42);. The explicit cast is still required when converting from a variable.

    hashtag
    Shielding Values (Unshielded to Shielded)

    When you cast from an unshielded type to a shielded type, you are "shielding" the value -- moving it from the public domain into confidential storage/computation.

    circle-exclamation

    Privacy consideration: When going from unshielded to shielded, the original unshielded value is visible in the transaction trace at the point of casting. Observers can see what value was shielded. The value only becomes confidential after the cast, in subsequent operations.

    If you need the value to be confidential from the start, it should arrive as encrypted calldata via a Seismic transaction (type 0x4A), not be cast from a public variable.

    hashtag
    Unshielding Values (Shielded to Unshielded)

    When you cast from a shielded type to an unshielded type, you are "unshielding" the value -- making it publicly visible.

    circle-exclamation

    Privacy consideration: When going from shielded to unshielded, the final unshielded value is visible in the transaction trace. Observers can see the result. This permanently exposes the value.

    This is sometimes necessary (e.g., returning a value from a view function or interfacing with a non-shielded contract), but you should be deliberate about when and why you do it.

    hashtag
    Casting saddress to Payable

    saddress payable is a valid type, but it does not unlock any extra operations — .transfer(), .send(), and .balance are all blocked on shielded addresses regardless of payability. The payable marker exists for type-system consistency (e.g., contracts with receive() payable convert to saddress payable), not for sending ETH.

    To actually send ETH to a shielded address, you must unshield it first:

    circle-exclamation

    Privacy consideration: Unshielding to address payable exposes the address in the transaction trace.

    You can also convert in the other direction:

    hashtag
    Size Casting Between Shielded Integers

    You can cast between different sizes of shielded integers, just as you can with regular Solidity integers:

    The same rules that apply to regular Solidity integer casting apply here:

    • Widening (smaller to larger): Always safe, the value is preserved.

    • Narrowing (larger to smaller): May truncate the value if it exceeds the target type's range.

    You can also cast between signed and unsigned shielded integers:

    hashtag
    Common Patterns

    hashtag
    Returning values from view functions

    Since shielded types cannot be returned from public or external functions, you must unshield them first:

    circle-info

    Returning an unshielded value from a view function makes it visible to the caller. If the caller should only see their own data, use access control and to ensure only authorized users can query it.

    hashtag
    Interfacing with non-shielded contracts

    When calling a contract that expects unshielded types, cast at the call boundary:

    hashtag
    Shielding input from encrypted calldata

    The only way to introduce a value that is private from the start is through encrypted calldata, where the value is never visible in plaintext on-chain:

    hashtag
    Security Implications

    Every cast between shielded and unshielded types is a potential information leak point. Keep these principles in mind:

    1. Unshielded-to-shielded casts expose the input value in the trace. If the value was meant to be secret from the start, use encrypted calldata instead.

    2. Shielded-to-unshielded casts expose the output value in the trace. Only unshield when you intend the value to become public.

    3. Minimize casts. The fewer times you cross the shielded/unshielded boundary, the smaller your attack surface.

    Installation

    Install seismic-viem and configure viem peer dependency

    hashtag
    Prerequisites

    Requirement
    Version
    Notes

    hashtag
    Install

    Install seismic-viem alongside its viem peer dependency using your preferred package manager:

    hashtag
    Dependencies

    seismic-viem has a single peer dependency:

    Package
    Version
    Role

    The following cryptographic libraries are bundled internally and do not need to be installed separately:

    Package
    Purpose
    circle-info

    The @noble/* packages handle all client-side cryptographic operations including ECDH key exchange with the TEE and AES-GCM calldata encryption. They are bundled as direct dependencies of seismic-viem, so you never need to install or import them yourself.

    hashtag
    Module Format

    seismic-viem ships as both ESM and CJS with a single export entry point. TypeScript type declarations (.d.ts) are included -- no separate @types/ package is needed.

    hashtag
    Minimal Working Example

    Create a new project and verify the installation:

    Create index.ts:

    Run it:

    If you see a block number printed, the installation is working correctly.

    hashtag
    TypeScript

    seismic-viem is written in TypeScript and ships its own type declarations. No additional configuration is required beyond a standard tsconfig.json:

    circle-info

    All exported types, interfaces, and function signatures are fully typed. Your editor will provide autocompletion and type checking for all seismic-viem APIs out of the box.

    hashtag
    Troubleshooting

    hashtag
    Peer Dependency Warning

    If you see a peer dependency warning for viem, ensure you have viem 2.x installed:

    hashtag
    Node.js Version

    seismic-viem requires Node.js 18 or newer for native fetch and crypto support:

    hashtag
    ESM/CJS Interop

    If you encounter module resolution issues, ensure your tsconfig.json uses "moduleResolution": "bundler" or "node16", and that your package.json has "type": "module" if using ESM.

    hashtag
    See Also

    • -- Configure network connections

    • -- Full SDK overview and architecture

    • -- React hooks layer built on seismic-viem

    Contract

    Instantiating contracts and interacting through shielded and transparent namespaces


    hashtag
    Instantiation

    contract = w3.seismic.contract(address="0x...", abi=ABI)

    The ABI works the same as in web3.py. If your contract uses shielded types (suint256, sbool, saddress), the SDK remaps them to their standard counterparts for parameter encoding while keeping the original shielded names for function selector computation.


    hashtag
    Namespaces

    ShieldedContract gives you five namespaces:

    Namespace
    What it does
    On-chain visibility

    Write namespaces accept optional keyword arguments for transaction parameters:


    hashtag
    Example Contract

    All code snippets in the Python SDK docs reference the interface below.

    Token examples use the real / ERC20 specs (balanceOf, transfer, approve, allowance, totalSupply, etc.).


    hashtag
    Encoding calldata manually

    If you need to encode calldata outside of a contract call — for example, to pass it to the — you can use . This computes the function selector using the original shielded type names (like suint256) but encodes the parameters using standard types (like uint256):

    ERC20 to SRC20: What Changes

    See exactly what changes between a standard ERC20 and a private SRC20

    This chapter puts a standard ERC20 and its SRC20 counterpart side by side so you can see exactly how little changes. To get privacy on Seismic, just change the types. Estimated time: ~10 minutes.

    hashtag
    The standard ERC20

    Here is a minimal but complete ERC20 contract. It covers the full interface -- name, symbol, decimals, totalSupply, balances, allowances, transfer, approve, and transferFrom:

    This is standard Solidity. Every balance, transfer amount, and allowance is a uint256

    Shielded Balances and Transfers

    Implement transfer() and transferFrom() with suint256

    This chapter walks through the full implementation of shielded transfers, allowances, and minting. You will also write tests using sforge to verify everything works. Estimated time: ~20 minutes.

    hashtag
    Shielded balanceOf

    The core change is in the mapping declaration:

    At the storage level, this is where Seismic's FlaggedStorage comes in. Each storage slot is a tuple of

    Ch 2: Stamina and Robbing Secrets

    In this chapter, you'll build the stamina bar, the protective layer that guards the clown's secrets. You'll initialize the clown's stamina and implement a hit function to reduce it. Additionally, you'll add a rob() function with a requireDown modifier to ensure secrets can only be stolen once the clown is knocked out. Estimated Time: ~10 minutes.

    hashtag
    Defining the stamina bar

    Deposit Contract

    Validator staking operations on the Seismic deposit contract

    The deposit contract is the entry point for validator staking on Seismic. It accepts deposits that register validators with their node and consensus keys, and exposes read methods for the current deposit root and count. Seismic ships client extensions for both sides.

    • depositContractPublicActions -- read-only: getDepositRoot(), getDepositCount()

    AppKit

    Set up AppKit (WalletConnect) with Seismic

    AppKit (formerly Web3Modal) by WalletConnect provides a wallet connection modal with support for 300+ wallets. This guide shows how to integrate AppKit with Seismic React.

    hashtag
    Prerequisites

    circle-exclamation

    You need a WalletConnect Project ID from

    AsyncPublicContract

    Async contract wrapper with transparent read-only access

    Asynchronous contract wrapper for read-only transparent access to Seismic contracts.

    hashtag
    Overview

    AsyncPublicContract is the async version of PublicContract, providing non-blocking read-only access to contract state. It exposes only the .tread namespace for standard async eth_call

    Ch 3: Bringing It All Together

    Now that we've built the core logic for interacting with the ClownBeatdown contract, it's time to tie everything together into a CLI that runs a multiplayer game session with two players — Alice and Bob. In this chapter, you'll set up the environment variables for multiple players and write index.ts to simulate gameplay. Estimated time: ~20 minutes

    hashtag
    Set Up Environment Variables

    Before running the game, we need to define environment variables that store important configurations such as the RPC URL, chain ID, and player private keys.

    string | null

    Error message if initialization failed

    loaded

    boolean

    Whether the wallet client has finished initializing

    useSignedReadContract
    Hooks Overview

    Yes

    Contract address (checksummed Ethereum address)

    abi

    list[dict[str, Any]]

    Yes

    Contract ABI (list of function entries)

    Contract Instance Guide

    No

    Block explorer URL

    Wallet Guides
    REST API

    useReadContract

    Execute authenticated read calls

    useSignedReadContract

    Execute authenticated read calls

    useShieldedWallet
    useShieldedContract
    useShieldedWriteContract

    31337

    Local Sanvil dev node

    Local Devnet

    localSeismicDevnet

    5124

    Local seismic-reth --dev

    Custom Devnet

    createSeismicDevnet()

    5124

    Factory for custom chain configs

    Seismic Testnet
    Sanvil
    Seismic Testnet
    Sanvil
    createSeismicDevnet
    RainbowKit
    Privy
    AppKit
    RainbowKit
    Privy
    AppKit

    Setting Up Your Project

    In this chapter, you'll set up the foundation for the Clown Beatdown game using a clean and modular monorepo-style workspace. This structure separates concerns into distinct areas, making your project easier to navigate and maintain. You'll create a contracts directory to house your Seismic smart contracts and tests, and a cli directory to serve as the command-line interface for interacting with those contracts. By the end of this chapter, you'll have a fully initialized project, complete with dependencies, formatting tools, and a seamless environment for both contract development and interaction.

    Audit cast points. During security review, identify every cast in your contract and verify that the information exposure at each point is intentional and acceptable.
    s suffix
    signed reads

    Node.js

    18+

    LTS recommended; install via nvmarrow-up-right or nodejs.orgarrow-up-right

    viem

    2.x

    Peer dependency -- installed alongside seismic-viem

    viem

    2.x

    Core Ethereum client library (transports, chains, ABIs)

    @noble/hashes

    SHA-256, HKDF, and other hash functions

    @noble/curves

    Elliptic curve operations (secp256k1, ECDH)

    @noble/ciphers

    AES-GCM encryption and decryption

    Chains
    Seismic Viem Overview
    seismic-react
    npm install seismic-viem viem
    yarn add seismic-viem viem
    pnpm add seismic-viem viem
    bun add seismic-viem viem

    Standard eth_sendTransaction

    Calldata plaintext

    .tread

    Standard eth_call

    Calldata plaintext

    .dwrite

    Debug write — like .write but returns plaintext + encrypted views

    Calldata encrypted

    .write

    Encrypted transaction (TxSeismic type 0x4a)

    Calldata encrypted

    .read

    Encrypted signed eth_call

    Calldata + result encrypted

    SRC20arrow-up-right
    low-level API
    encode_shielded_calldataarrow-up-right

    .twrite

    , visible to anyone who queries the contract or reads the chain.

    hashtag
    The SRC20 version

    Here is the same contract converted to an SRC20. Changed lines are marked with comments:

    That is the entire diff. The contract logic is structurally identical.

    hashtag
    Line-by-line diff

    hashtag
    Balances: uint256 to suint256

    Two changes here. First, the value type changes from uint256 to suint256. This tells the Seismic compiler to emit CSTORE/CLOAD instead of SSTORE/SLOAD, which marks these storage slots as private. Second, the public visibility modifier is removed. Shielded types cannot be returned from public or external functions, so the automatic getter that public generates would not compile. We will add an explicit balance-checking function using signed reads in a later chapter.

    hashtag
    Allowances: same pattern

    The same change applies to the allowance mapping. The nested mapping's value type becomes suint256, and the public modifier is removed.

    hashtag
    Function parameters: shielded amounts

    The amount parameter changes to suint256. When a user calls this function through a Seismic transaction (type 0x4A), the amount is encrypted in the calldata before it leaves their machine. During execution inside the TEE, the amount is decrypted and used normally. Observers watching the mempool or block data see 0x00...0 in place of the amount.

    The same change applies to approve and transferFrom.

    hashtag
    Constructor: casting the initial supply

    Since _initialSupply is a regular uint256 (it is the total supply, which is public) and balanceOf now stores suint256 values, an explicit cast is required. Seismic does not allow implicit casting between shielded and unshielded types.

    hashtag
    Events: casting back to uint256

    Events are stored in transaction logs, which are public. Shielded types cannot appear in event parameters. The simplest approach is to cast the amount back to uint256 before emitting. Note that this does reveal the amount in the event log. If you need the event data to also be private, the Encrypted Events chapter shows how to use AES-GCM precompiles to encrypt it.

    hashtag
    What stays the same

    A lot stays the same, which is the point:

    • Transfer logic -- The subtraction, addition, and require checks are identical. Arithmetic operations on suint256 work the same as on uint256.

    • Overflow protection -- Solidity 0.8+ overflow checks work with shielded types.

    • Function signatures -- The function names and return types are unchanged. The contract is still recognizable as an ERC20.

    • Address parameters -- The to, from, and spender parameters remain regular address types. Mapping keys cannot be shielded types, so these must stay as address.

    • totalSupply -- This stays as a regular uint256. The total supply is public information. Individual balances are private, but the aggregate is visible.

    hashtag
    What you lose (and how to get it back)

    There are two capabilities that the basic SRC20 loses compared to a standard ERC20:

    hashtag
    1. Public balance queries

    Since balanceOf cannot be public, there is no automatic getter. Users cannot call balanceOf(address) the way they would with a standard ERC20. The solution is signed reads -- a Seismic-specific mechanism where a user sends a signed eth_call (type 0x4A) to prove their identity, and the contract returns their balance only to them. This is covered in Signed Reads for Balance Checking.

    hashtag
    2. Private event data

    With the simple cast approach above, the amount appears in plaintext in the event log. If you need transfer amounts to be hidden in events as well, you can encrypt the data using Seismic's AES-GCM precompiles before emitting. This is covered in Encrypted Events.

    Both of these are straightforward additions. The next chapters walk through each one.

    (value, is_private)
    . When the compiler sees
    suint256
    , it emits
    CSTORE
    to write and
    CLOAD
    to read, setting the
    is_private
    flag to
    true
    . This means:
    • eth_getStorageAt calls for these slots will fail. External observers cannot read the raw storage.

    • Only CLOAD can access private slots. The standard SLOAD opcode cannot reach them.

    • Anyone inspecting the state trie, transaction traces, or block data sees 0x00...0 in place of the actual balance.

    The developer does not interact with FlaggedStorage directly. The type annotation handles everything.

    hashtag
    transfer()

    Here is the full transfer implementation with shielded amounts:

    hashtag
    What happens at each stage

    1. Calldata submission -- The user sends a Seismic transaction (type 0x4A). The amount parameter is encrypted before it leaves their machine. Observers watching the mempool see 0x00...0 in place of the amount.

    2. Execution inside the TEE -- The Seismic node, running inside Intel TDX, decrypts the calldata. The require check runs against the shielded balance. The subtraction and addition execute normally. All intermediate values involving suint256 are shielded in the trace.

    3. Storage update -- The new balances are written via CSTORE. Both the sender's and recipient's balance slots have is_private = true.

    4. Observer view -- Anyone querying the contract or reading the block sees 0x00...0 for the amount, the sender's balance, and the recipient's balance.

    Comparisons (>=) and arithmetic (-=, +=) work the same on suint256 as on uint256. Solidity 0.8+ overflow checks also work, so if a user tries to transfer more than their balance, the transaction reverts as expected.

    hashtag
    transferFrom()

    The full implementation with shielded allowances:

    The allowance mapping stores suint256 values:

    The pattern is identical to a standard ERC20 transferFrom. The only difference is the type. The allowance check, the allowance deduction, and the balance updates all use shielded arithmetic. An observer cannot see how much allowance was granted, how much was consumed, or how much remains.

    hashtag
    approve()

    Setting shielded allowances:

    The approved amount is stored as suint256. No one can query how much a spender is authorized to transfer on behalf of the owner -- that information is shielded in storage. The Approval event above casts the amount to uint256 for the log. If you need the approved amount to be private in the event as well, see the Encrypted Events chapter.

    hashtag
    The mint pattern

    Here is a mint function that assigns new tokens:

    There is a design decision here: totalSupply is public. It is a regular uint256, so the aggregate supply is visible. This is usually desirable -- users and markets want to know how many tokens exist. However, individual balances remain shielded. An observer knows the total supply increased, but cannot see which address received the tokens or how they are distributed.

    If you want even the total supply to be private, you can change it to suint256:

    But keep in mind that suint256 state variables cannot be public, so you would need to provide a view function that determines who can view it via signed reads.

    hashtag
    Constructor minting

    The simplest approach is to mint the entire initial supply in the constructor:

    The explicit cast suint256(_initialSupply) is required because _initialSupply is a regular uint256. Seismic enforces explicit casting between shielded and unshielded types.

    hashtag
    Testing with sforge

    Create a test file at test/SRC20.t.sol:

    hashtag
    Test: basic transfer

    circle-info

    In sforge tests, the test contract runs inside the same execution context. You can read shielded values by adding an internal helper function to your contract (or using the test contract's access). In production, shielded values are only accessible through signed reads.

    To support this test, add a test-only helper to your contract:

    hashtag
    Test: transfer reverts on insufficient balance

    hashtag
    Test: approve and transferFrom

    hashtag
    Test: transferFrom reverts on insufficient allowance

    hashtag
    Running the tests

    Build and run from your contracts directory:

    You should see all tests passing. The shielded types behave identically to their unshielded counterparts in terms of arithmetic and comparison logic -- the privacy is handled transparently at the storage layer.

    The stamina bar determines the clown's resilience. It has an integer value (clownStamina), which represents how many hits the clown can withstand before going down. Let's define the stamina and initialize it in the constructor:

    hashtag
    Adding the hit function

    Each time the clown is hit, its stamina decreases, bringing it one step closer to being knocked out. This is crucial for revealing secrets, as the clown must be fully knocked out for secrets to be accessible:

    hashtag
    What's happening here?

    • The requireStanding modifier: Ensures that the function cannot be called if the clown is already knocked out (clownStamina == 0). This prevents unnecessary calls after the clown is down.

    • Decrementing stamina: Each call to hit decreases the clown's stamina (clownStamina) by one.

    • Logging the action: The Hit event records the round, the hitter's address (msg.sender), and the remaining stamina.

    hashtag
    Adding a stamina getter

    Add a public view function so the current stamina can be checked:

    hashtag
    Robbing the clown

    Now that we have implemented the stamina system and the ability to reduce it using the hit function, we can introduce the robbery mechanic: the clown's secret should only be revealed once it is fully knocked out.

    • The secret should remain hidden while the clown is standing.

    • The secret can only be stolen once the clown's stamina reaches zero, i.e. when it is knocked out.

    To enforce this, we will create a function called rob(), which will return a randomly selected secret, but only if the clown has been fully knocked out.

    Here's how we define rob() with a requireDown modifier:

    hashtag
    What's happening here?

    • Restricting Access with a Condition: The requireDown modifier ensures that rob() can only be called if clownStamina == 0, meaning the clown has been fully knocked out.

    • Revealing the Secret: Once the condition is met, rob() reads the secret at the shielded secretIndex position. It converts the sbytes value to plain bytes for the caller.

    • Preventing Premature Access: If rob() is called before the clown is knocked out, the function will revert with the error "CLOWN_STILL_STANDING".

    hashtag
    Updated contract with hit and rob

    Create a .env in packages/cli:

    Open .env and paste the following:

    What's Happening Here?

    • CHAIN_ID=31337: Used to locate the deployment broadcast file for the correct chain.

    • VITE_CHAIN_ID=31337: Used for chain selection (sanvil vs testnet). 31337 is the default chain ID for sanvil (your local Seismic node).

    • RPC_URL=http://127.0.0.1:8545: This is the RPC URL for interacting with the local Seismic node.

    • ALICE_PRIVKEY and BOB_PRIVKEY: These are Alice and Bob's private keys, allowing them to play the game. (These are standard test keys provided by sanvil)

    hashtag
    Write index.ts

    Now, we'll create the main entry point for our game session. This file will simulate gameplay, initializing players and having them interact with the ClownBeatdown contract. Open packages/cli/src/index.ts and follow these steps:

    hashtag
    Import Dependencies

    Import the required libraries to read environment variables, define network configurations, and interact with the ClownBeatdown contract.

    hashtag
    Define the main() function

    This function initializes the contract and player wallets, then runs the game session.

    hashtag
    Read Contract Details

    The contract's ABI and deployed address are read from files generated during deployment.

    hashtag
    Select the blockchain network

    Determine whether to use the local sanvil node (31337) or the Seismic testnet.

    hashtag
    Define players

    Assign Alice and Bob as players with private keys stored in .env.

    hashtag
    Initialize the Game App

    Create an App instance to interact with the ClownBeatdown contract.

    hashtag
    Simulate the game round by round

    The following logic executes two rounds of gameplay between Alice and Bob.

    Round 1 — Alice Plays

    Alice hits the clown three times (stamina starts at 3), knocking it out, then robs a secret.

    Round 2 — Bob Plays

    Bob resets the clown, hits it three more times, then robs his own secret.

    Alice Tries to Rob in Round 2 (we expect this to fail since she contributed in round 1 but not round 2)

    hashtag
    Execute the main() function

    This ensures that the script runs when executed.

    hashtag
    Running the CLI

    Now, run the CLI from packages/cli by running:

    You should see something like this as the output:

    This output logs the events during two rounds of gameplay in the ClownBeatdown contract, showing interactions by Alice and Bob, along with a revert error when Alice attempts to call rob() in Round 2.

    Congratulations! You've reached the end of the tutorial.

    function test_Hit() public {
        clownBeatdown.hit();
        assertEq(clownBeatdown.getClownStamina(), 1);
    }
    function test_KnockoutAndRob() public {
        clownBeatdown.hit();
        clownBeatdown.hit();
        // rob() should return one of the secrets
        bytes memory secret = clownBeatdown.rob();
        assertTrue(
            keccak256(secret) == keccak256(bytes("Secret A")) ||
            keccak256(secret) == keccak256(bytes("Secret B")) ||
            keccak256(secret) == keccak256(bytes("Secret C"))
        );
    }
    function test_Reset() public {
        clownBeatdown.hit();
        clownBeatdown.hit();
        clownBeatdown.reset();
        assertEq(clownBeatdown.getClownStamina(), 2); // Stamina should be reset to 2
    }
    function test_SecretCanChangeAfterReset() public {
        // Knock out and rob in round 1
        clownBeatdown.hit();
        clownBeatdown.hit();
        bytes memory secret1 = clownBeatdown.rob();
    
        // Reset and knock out again in round 2
        clownBeatdown.reset();
        clownBeatdown.hit();
        clownBeatdown.hit();
        bytes memory secret2 = clownBeatdown.rob();
    
        // Both should be valid secrets (they may or may not differ depending on randomness)
        assertTrue(
            keccak256(secret1) == keccak256(bytes("Secret A")) ||
            keccak256(secret1) == keccak256(bytes("Secret B")) ||
            keccak256(secret1) == keccak256(bytes("Secret C"))
        );
        assertTrue(
            keccak256(secret2) == keccak256(bytes("Secret A")) ||
            keccak256(secret2) == keccak256(bytes("Secret B")) ||
            keccak256(secret2) == keccak256(bytes("Secret C"))
        );
    }
    function test_CannotHitWhenDown() public {
        clownBeatdown.hit();
        clownBeatdown.hit();
        vm.expectRevert("CLOWN_ALREADY_DOWN");
        clownBeatdown.hit();
    }
    function test_CannotRobWhenStanding() public {
        clownBeatdown.hit();
        vm.expectRevert("CLOWN_STILL_STANDING");
        clownBeatdown.rob();
    }
    function test_CannotResetWhenStanding() public {
        vm.expectRevert("CLOWN_STILL_STANDING");
        clownBeatdown.reset();
    }
    function test_RevertWhen_NonContributorTriesToRob() public {
        address nonContributor = address(0xabcd);
    
        // Knock out the clown
        clownBeatdown.hit();
        clownBeatdown.hit();
    
        // Non-contributor should be rejected
        vm.prank(nonContributor);
        vm.expectRevert("NOT_A_CONTRIBUTOR");
        clownBeatdown.rob();
    
        // Original contributor can still rob
        bytes memory secret = clownBeatdown.rob();
        assertTrue(secret.length > 0);
    }
    function test_ContributorInRound2() public {
        address contributorRound2 = address(0xabcd);
    
        // Round 1: knocked out by address(this)
        clownBeatdown.hit();
        clownBeatdown.hit();
        bytes memory secret1 = clownBeatdown.rob();
        assertTrue(secret1.length > 0);
    
        // Reset for round 2
        clownBeatdown.reset();
    
        // Round 2: knocked out by contributorRound2
        vm.prank(contributorRound2);
        clownBeatdown.hit();
        vm.prank(contributorRound2);
        clownBeatdown.hit();
    
        // contributorRound2 can rob in round 2
        vm.prank(contributorRound2);
        bytes memory secret2 = clownBeatdown.rob();
        assertTrue(secret2.length > 0);
    
        // address(this) cannot rob in round 2 (not a contributor this round)
        vm.expectRevert("NOT_A_CONTRIBUTOR");
        clownBeatdown.rob();
    }
    sforge build
    sforge test
    bool flag = true;
    sbool shieldedFlag = sbool(flag);          // OK
    
    address addr = msg.sender;
    saddress shieldedAddr = saddress(addr);    // OK
    
    int256 signed = -42;
    sint256 shieldedSigned = sint256(signed);  // OK
    
    bytes32 hash = keccak256("secret");
    sbytes32 shieldedHash = sbytes32(hash);    // OK
    uint256 publicValue = 100;
    suint256 shieldedValue = suint256(publicValue);
    suint256 shieldedValue = /* ... */;
    uint256 publicValue = uint256(shieldedValue);
    address payable exposed = payable(address(someSaddressValue));
    exposed.transfer(1 ether);
    address payable pay = /* ... */;
    saddress shielded = saddress(address(pay));
    suint128 smaller = suint128(42);
    suint256 larger = suint256(smaller);  // Widening: safe, no data loss
    
    suint256 big = suint256(1000);
    suint128 small = suint128(big);       // Narrowing: may truncate
    suint256 unsigned = suint256(100);
    sint256 signed = sint256(unsigned);   // Reinterprets the bits
    suint256 private balance;
    
    // Will NOT compile -- cannot return shielded type from external function
    function getBalance() external view returns (suint256) { ... }
    
    // Correct -- unshield before returning
    function getBalance() external view returns (uint256) {
        return uint256(balance);
    }
    suint256 private amount;
    
    function sendToExternal(address externalContract) external {
        IExternalContract(externalContract).deposit(uint256(amount));
    }
    // The `amount` parameter arrives encrypted via a Seismic transaction (0x4A)
    // and is decrypted inside the TEE -- it is never publicly visible.
    function deposit(suint256 amount) external {
        balances[msg.sender] += amount;
    }
    // Example: Unintended leak through intermediate cast
    suint256 private secretA;
    suint256 private secretB;
    
    function leakyCompare() external view returns (bool) {
        // BAD: Both secrets are unshielded just to compare them publicly
        return uint256(secretA) > uint256(secretB);
    }
    
    function safeCompare() external view returns (sbool) {
        // BETTER: Compare in the shielded domain, no information leaves
        return secretA > secretB;
        // Note: sbool cannot be returned from external -- this is illustrative.
        // In practice, you would use the comparison result internally.
    }
    // ESM (recommended)
    import { createShieldedWalletClient, seismicTestnet } from "seismic-viem";
    
    // CJS
    const { createShieldedWalletClient, seismicTestnet } = require("seismic-viem");
    mkdir my-seismic-app && cd my-seismic-app
    npm init -y
    npm install seismic-viem viem
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { createShieldedWalletClient, seismicTestnet } from "seismic-viem";
    
    const client = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    
    const blockNumber = await client.getBlockNumber();
    console.log("Connected! Block:", blockNumber);
    npx tsx index.ts
    {
      "compilerOptions": {
        "target": "ES2022",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "strict": true
      }
    }
    npm ls viem
    # Should show [email protected]
    node --version
    # Should be >= v18.0.0
    # Shielded write — encrypted calldata, returns tx hash
    tx_hash = contract.write.setNumber(42)
    
    # Shielded read — encrypted signed call, auto-decoded
    number = contract.read.getNumber()       # int
    is_odd = contract.read.isOdd()           # bool
    
    # Transparent write — standard send_transaction
    tx_hash = contract.twrite.setNumber(42)
    
    # Transparent read — standard eth_call, auto-decoded
    number = contract.tread.getNumber()      # int
    
    # Debug write — returns plaintext + encrypted views + tx hash
    debug = contract.dwrite.setNumber(42)
    debug.plaintext_tx.data  # unencrypted calldata
    debug.shielded_tx.data   # encrypted calldata
    debug.tx_hash            # transaction hash
    tx_hash = contract.write.deposit(value=10**18, gas=100_000, gas_price=10**9)
    interface IExampleVault {
        // ── Public reads (no msg.sender dependency → .tread) ─────
        function getNumber()       external view returns (uint256);
        function isOdd()           external view returns (bool);
        function isActive()        external view returns (bool);
        function getName()         external view returns (string memory);
        function getConfig()       external view returns (uint256 maxDeposit, uint256 feeRate, bool paused);
        function getHolders()      external view returns (address[] memory);
        function getItemCount()    external view returns (uint256);
        function getItems(uint256 offset, uint256 limit)
            external view returns (uint256[] memory);
        function getUserInfo(address user)
            external view returns (string memory name, uint256 balance, bool active);
    
        // ── Shielded reads (use msg.sender → require .read) ──────
        function getSecretBalance()  external view returns (suint256);
    
        // ── Writes ───────────────────────────────────────────────
        function setNumber(uint256 value)  external;
        function deposit()                 external payable;
        function withdraw(suint256 amount) external;
        function batchTransfer(
            address[] calldata recipients,
            suint256[] calldata amounts
        ) external;
    }
    from seismic_web3.contract.abi import encode_shielded_calldata
    
    data = encode_shielded_calldata(abi, "setNumber", [42])
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.13;
    
    contract ERC20 {
        string public name;
        string public symbol;
        uint8 public decimals = 18;
        uint256 public totalSupply;
    
        mapping(address => uint256) public balanceOf;
        mapping(address => mapping(address => uint256)) public allowance;
    
        event Transfer(address indexed from, address indexed to, uint256 amount);
        event Approval(address indexed owner, address indexed spender, uint256 amount);
    
        constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
            name = _name;
            symbol = _symbol;
            totalSupply = _initialSupply;
            balanceOf[msg.sender] = _initialSupply;
        }
    
        function transfer(address to, uint256 amount) public returns (bool) {
            require(balanceOf[msg.sender] >= amount, "Insufficient balance");
            balanceOf[msg.sender] -= amount;
            balanceOf[to] += amount;
            emit Transfer(msg.sender, to, amount);
            return true;
        }
    
        function approve(address spender, uint256 amount) public returns (bool) {
            allowance[msg.sender][spender] = amount;
            emit Approval(msg.sender, spender, amount);
            return true;
        }
    
        function transferFrom(address from, address to, uint256 amount) public returns (bool) {
            require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
            require(balanceOf[from] >= amount, "Insufficient balance");
            allowance[from][msg.sender] -= amount;
            balanceOf[from] -= amount;
            balanceOf[to] += amount;
            emit Transfer(from, to, amount);
            return true;
        }
    }
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.13;
    
    contract SRC20 {
        string public name;
        string public symbol;
        uint8 public decimals = 18;
        uint256 public totalSupply;                                          // stays public
    
        mapping(address => suint256) balanceOf;                              // CHANGED: uint256 -> suint256, removed public
        mapping(address => mapping(address => suint256)) allowance;          // CHANGED: uint256 -> suint256, removed public
    
        event Transfer(address indexed from, address indexed to, uint256 amount);
        event Approval(address indexed owner, address indexed spender, uint256 amount);
    
        constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
            name = _name;
            symbol = _symbol;
            totalSupply = _initialSupply;
            balanceOf[msg.sender] = suint256(_initialSupply);                // CHANGED: cast to suint256
        }
    
        function transfer(address to, suint256 amount) public returns (bool) {   // CHANGED: uint256 -> suint256
            require(balanceOf[msg.sender] >= amount, "Insufficient balance");
            balanceOf[msg.sender] -= amount;
            balanceOf[to] += amount;
            emit Transfer(msg.sender, to, uint256(amount));                  // CHANGED: cast back for event
            return true;
        }
    
        function approve(address spender, suint256 amount) public returns (bool) {  // CHANGED: uint256 -> suint256
            allowance[msg.sender][spender] = amount;
            emit Approval(msg.sender, spender, uint256(amount));             // CHANGED: cast back for event
            return true;
        }
    
        function transferFrom(address from, address to, suint256 amount) public returns (bool) {  // CHANGED: uint256 -> suint256
            require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
            require(balanceOf[from] >= amount, "Insufficient balance");
            allowance[from][msg.sender] -= amount;
            balanceOf[from] -= amount;
            balanceOf[to] += amount;
            emit Transfer(from, to, uint256(amount));                        // CHANGED: cast back for event
            return true;
        }
    }
    - mapping(address => uint256) public balanceOf;
    + mapping(address => suint256) balanceOf;
    - mapping(address => mapping(address => uint256)) public allowance;
    + mapping(address => mapping(address => suint256)) allowance;
    - function transfer(address to, uint256 amount) public returns (bool) {
    + function transfer(address to, suint256 amount) public returns (bool) {
    - balanceOf[msg.sender] = _initialSupply;
    + balanceOf[msg.sender] = suint256(_initialSupply);
    - emit Transfer(msg.sender, to, amount);
    + emit Transfer(msg.sender, to, uint256(amount));
    mapping(address => suint256) balanceOf;
    function transfer(address to, suint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
        emit Transfer(msg.sender, to, uint256(amount));
        return true;
    }
    function transferFrom(address from, address to, suint256 amount) public returns (bool) {
        require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
        require(balanceOf[from] >= amount, "Insufficient balance");
    
        allowance[from][msg.sender] -= amount;
        balanceOf[from] -= amount;
        balanceOf[to] += amount;
    
        emit Transfer(from, to, uint256(amount));
        return true;
    }
    mapping(address => mapping(address => suint256)) allowance;
    function approve(address spender, suint256 amount) public returns (bool) {
        allowance[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, uint256(amount));
        return true;
    }
    function mint(address to, suint256 amount) public {
        // In production, add access control here (e.g., onlyOwner)
        totalSupply += uint256(amount);
        balanceOf[to] += amount;
        emit Transfer(address(0), to, uint256(amount));
    }
    suint256 totalSupply;
    constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
        name = _name;
        symbol = _symbol;
        totalSupply = _initialSupply;
        balanceOf[msg.sender] = suint256(_initialSupply);
    }
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.13;
    
    import {Test, console} from "forge-std/Test.sol";
    import {SRC20} from "../src/SRC20.sol";
    
    contract SRC20Test is Test {
        SRC20 public token;
        address public alice;
        address public bob;
    
        function setUp() public {
            token = new SRC20("Shielded Token", "SRC", 1000000e18);
            alice = address(this);       // deployer holds initial supply
            bob = address(0xB0B);
        }
    }
    function test_Transfer() public {
        // Transfer 100 tokens from alice (deployer) to bob
        bool success = token.transfer(bob, 100e18s);
        assertTrue(success);
    
        // Verify balances using the internal test helper
        // In sforge tests, the test contract can read shielded values directly
        assertEq(token.getBalanceForTest(bob), 100e18);
        assertEq(token.getBalanceForTest(alice), 999900e18);
    }
    // Only for testing -- remove before deployment
    function getBalanceForTest(address account) external view returns (uint256) {
        return uint256(balanceOf[account]);
    }
    function test_RevertWhen_InsufficientBalance() public {
        vm.prank(bob); // bob has no tokens
        vm.expectRevert("Insufficient balance");
        token.transfer(alice, suint256(1e18));
    }
    function test_TransferFrom() public {
        // Alice approves bob to spend 500 tokens
        token.approve(bob, 500e18s);
    
        // Bob transfers 200 tokens from alice to himself
        vm.prank(bob);
        bool success = token.transferFrom(alice, bob, 200e18s);
        assertTrue(success);
    
        // Verify balances
        assertEq(token.getBalanceForTest(bob), 200e18);
        assertEq(token.getBalanceForTest(alice), 999800e18);
    }
    function test_RevertWhen_InsufficientAllowance() public {
        token.approve(bob, suint256(50e18));
    
        vm.prank(bob);
        vm.expectRevert("Insufficient allowance");
        token.transferFrom(alice, bob, suint256(100e18));
    }
    sforge test
        uint256 initialClownStamina; // Starting stamina restored on reset.
        uint256 clownStamina; // Remaining stamina before the clown is down.
    
        constructor(uint256 _clownStamina) {
            initialClownStamina = _clownStamina; // Set starting stamina.
            clownStamina = _clownStamina; // Initialize remaining stamina.
            round = 1; // Start with the first round.
        }
        // Event to log hits.
        event Hit(uint256 indexed round, address indexed hitter, uint256 remaining);
    
        // Hit the clown to reduce stamina.
        function hit() public requireStanding {
            clownStamina--; // Decrease stamina.
            emit Hit(round, msg.sender, clownStamina); // Log the hit.
        }
    
        // Modifier to ensure the clown is still standing.
        modifier requireStanding() {
            require(clownStamina > 0, "CLOWN_ALREADY_DOWN");
            _;
        }
        // Get the current clown stamina.
        function getClownStamina() public view returns (uint256) {
            return clownStamina;
        }
        // Reveal secret once the clown is down and the caller contributed.
        function rob() public view requireDown returns (bytes memory) {
            sbytes memory secret = secrets[uint256(secretIndex)];
            return bytes(secret); // Return the randomly selected secret.
        }
    
        // Modifier to ensure the clown is down.
        modifier requireDown() {
            require(clownStamina == 0, "CLOWN_STILL_STANDING");
            _;
        }
    // SPDX-License-Identifier: MIT License
    pragma solidity ^0.8.13;
    
    contract ClownBeatdown {
        uint256 initialClownStamina; // Starting stamina restored on reset.
        uint256 clownStamina; // Remaining stamina before the clown is down.
        uint256 round; // The current round number.
    
        mapping(uint256 => sbytes) secrets; // Pool of possible secrets (shielded).
        uint256 secretsCount; // Number of secrets for modular arithmetic.
        suint256 secretIndex; // Shielded index into the secrets mapping.
    
        // Event to log hits.
        event Hit(uint256 indexed round, address indexed hitter, uint256 remaining);
    
        constructor(uint256 _clownStamina) {
            initialClownStamina = _clownStamina; // Set starting stamina.
            clownStamina = _clownStamina; // Initialize remaining stamina.
            round = 1; // Start with the first round.
        }
    
        // Get the current clown stamina.
        function getClownStamina() public view returns (uint256) {
            return clownStamina;
        }
    
        function addSecret(string memory _secret) public {
            secrets[secretsCount] = sbytes(_secret);
            secretsCount++;
            secretIndex = suint256(_randomIndex()); // Re-pick a random secret.
        }
    
        // Hit the clown to reduce stamina.
        function hit() public requireStanding {
            clownStamina--; // Decrease stamina.
            emit Hit(round, msg.sender, clownStamina); // Log the hit.
        }
    
        // Reveal secret once the clown is down.
        function rob() public view requireDown returns (bytes memory) {
            sbytes memory secret = secrets[uint256(secretIndex)];
            return bytes(secret); // Return the randomly selected secret.
        }
    
        // Generate a pseudo-random index into the secrets array.
        function _randomIndex() private view returns (uint256) {
            return uint256(keccak256(abi.encodePacked(block.prevrandao, block.timestamp, round))) % secretsCount;
        }
    
        // Modifier to ensure the clown is down.
        modifier requireDown() {
            require(clownStamina == 0, "CLOWN_STILL_STANDING");
            _;
        }
    
        // Modifier to ensure the clown is still standing.
        modifier requireStanding() {
            require(clownStamina > 0, "CLOWN_ALREADY_DOWN");
            _;
        }
    }
    touch .env
    CHAIN_ID=31337
    VITE_CHAIN_ID=31337
    RPC_URL=http://127.0.0.1:8545
    ALICE_PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    BOB_PRIVKEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
    import dotenv from "dotenv";
    import { join } from "path";
    import { sanvil, seismicTestnet } from "seismic-viem";
    
    import { CONTRACT_DIR, CONTRACT_NAME } from "../lib/constants";
    import { readContractABI, readContractAddress } from "../lib/utils";
    import { App } from "./app";
    
    // Load environment variables from .env file
    dotenv.config();
    async function main() {
      if (!process.env.CHAIN_ID || !process.env.RPC_URL) {
        console.error('Please set your environment variables.')
        process.exit(1)
      }
    const broadcastFile = join(
      CONTRACT_DIR,
      "broadcast",
      `${CONTRACT_NAME}.s.sol`,
      process.env.CHAIN_ID,
      "run-latest.json",
    );
    const abiFile = join(
      CONTRACT_DIR,
      "out",
      `${CONTRACT_NAME}.sol`,
      `${CONTRACT_NAME}.json`,
    );
    const chain =
      process.env.VITE_CHAIN_ID === sanvil.id.toString() ? sanvil : seismicTestnet;
    const players = [
      { name: "Alice", privateKey: process.env.ALICE_PRIVKEY! },
      { name: "Bob", privateKey: process.env.BOB_PRIVKEY! },
    ];
    const app = new App({
      players,
      wallet: {
        chain,
        rpcUrl: process.env.RPC_URL!,
      },
      contract: {
        abi: readContractABI(abiFile),
        address: readContractAddress(broadcastFile),
      },
    });
    
    await app.init();
    console.log("=== Round 1 ===");
    await app.hit("Alice");
    await app.hit("Alice");
    await app.hit("Alice");
    
    // Alice robs the clown's secret in round 1
    await app.rob("Alice");
    console.log("=== Round 2 ===");
    await app.reset("Bob");
    await app.hit("Bob");
    await app.hit("Bob");
    await app.hit("Bob");
    
    // Bob robs the clown's secret in round 2
    await app.rob("Bob");
      // Alice tries to rob in round 2, should fail by reverting
      console.log('=== Testing Access Control ===')
      console.log("Attempting Alice's rob() in Bob's round (should revert)")
      try {
        await app.rob('Alice')
        console.error('Expected rob() to revert but it succeeded')
        process.exit(1)
      } catch (error) {
        console.log('Received expected revert')
      }
    }
    main();
    bun dev
    === Round 1 ===
    - Player Alice writing hit()
    - Player Alice writing hit()
    - Player Alice writing hit()
    - Player Alice reading rob()
    - Player Alice robbed secret: The cake is a lie
    === Round 2 ===
    - Player Bob writing reset()
    - Player Bob writing hit()
    - Player Bob writing hit()
    - Player Bob writing hit()
    - Player Bob reading rob()
    - Player Bob robbed secret: 42 is the answer
    === Testing Access Control ===
    Attempting Alice's rob() in Bob's round (should revert)
    Received expected revert
    depositContractWalletActions -- write: deposit()

    Both extensions are already applied by createShieldedPublicClient and createShieldedWalletClient; you can also extend a vanilla viem client with them manually.

    hashtag
    Import

    DEPOSIT_CONTRACT_ADDRESS is 0x00000000219ab540356cBB839Cbe05303d7705Fa. Every action accepts an optional address to override this default.

    hashtag
    Extending a Client

    hashtag
    Public Actions

    hashtag
    getDepositRoot

    Returns the current deposit merkle root.

    Parameter
    Type
    Required
    Description

    address

    Address

    No

    Deposit contract address (defaults to the canonical)

    Returns: Promise<Hex> -- the SHA-256 deposit root.

    hashtag
    getDepositCount

    Returns the total number of deposits accepted by the contract.

    Parameter
    Type
    Required
    Description

    address

    Address

    No

    Deposit contract address (defaults to the canonical)

    Returns: Promise<Hex> -- the deposit count encoded as a little-endian 64-bit number.

    hashtag
    Wallet Actions

    hashtag
    deposit

    Registers a validator by submitting a deposit transaction.

    Parameter
    Type
    Required
    Description

    nodePubkey

    Hex

    Yes

    Validator ED25519 public key

    consensusPubkey

    Hex

    Returns: Promise<Hex> -- the transaction hash.

    circle-info

    depositDataRoot is verified on-chain against the supplied keys and signatures. If the root does not match the SSZ-encoded deposit data, the transaction reverts. This prevents malformed deposits from being silently accepted.

    hashtag
    See Also

    • Shielded Public Client -- base client that already includes these public actions

    • Shielded Wallet Client -- base client that already includes these wallet actions

    import {
      DEPOSIT_CONTRACT_ADDRESS,
      depositContractPublicActions,
      depositContractWalletActions,
    } from "seismic-viem";
    import {
      createShieldedPublicClient,
      createShieldedWalletClient,
      depositContractPublicActions,
      depositContractWalletActions,
      seismicTestnet,
    } from "seismic-viem";
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    
    const publicClient = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    }).extend(depositContractPublicActions);
    
    const walletClient = (
      await createShieldedWalletClient({
        chain: seismicTestnet,
        transport: http(),
        account: privateKeyToAccount("0x..."),
      })
    ).extend(depositContractWalletActions);
    const depositRoot = await publicClient.getDepositRoot({});
    const depositCount = await publicClient.getDepositCount({});
    import { parseEther } from "viem";
    
    const txHash = await walletClient.deposit({
      nodePubkey: "0x...", // ED25519 public key (32 bytes)
      consensusPubkey: "0x...", // BLS12-381 public key (48 bytes)
      withdrawalCredentials: "0x...", // commitment to withdrawal pubkey
      nodeSignature: "0x...", // ED25519 signature (64 bytes)
      consensusSignature: "0x...", // BLS12-381 signature (96 bytes)
      depositDataRoot: "0x...", // SHA-256 of SSZ-encoded DepositData
      value: parseEther("32"),
    });
    .

    hashtag
    Step 1: Configure wagmi with AppKit

    Create a wagmi adapter and initialize AppKit with Seismic chains:

    hashtag
    Step 2: Set Up Providers

    Nest the providers with ShieldedWalletProvider inside:

    circle-info

    AppKit does not require a wrapper provider component like RainbowKit or Privy. The createAppKit call initializes it globally, so ShieldedWalletProvider goes directly inside QueryClientProvider.

    hashtag
    Step 3: Add the Connect Button

    AppKit provides a web component for the connect button:

    circle-info

    If you are using TypeScript, you may need to declare the web component type. Add this to a .d.ts file in your project:

    declare namespace JSX {
      interface IntrinsicElements {
        "appkit-button": React.DetailedHTMLProps<
          React.HTMLAttributes<HTMLElement>,
          HTMLElement
        >;
      }
    }

    hashtag
    Step 4: Use Shielded Hooks

    Once connected, use seismic-react hooks to interact with shielded contracts:

    hashtag
    Complete Example

    hashtag
    See Also

    • Wallet Guides Overview -- Comparison of wallet libraries

    • ShieldedWalletProvider -- Provider reference and options

    • useShieldedWallet -- Access shielded wallet context

    • -- Polished wallet modal alternative

    • -- Email/social login alternative

    npm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query seismic-react seismic-viem
    import { createAppKit } from "@reown/appkit/react";
    import { WagmiAdapter } from "@reown/appkit-adapter-wagmi";
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    const projectId = "YOUR_WALLETCONNECT_PROJECT_ID";
    
    const wagmiAdapter = new WagmiAdapter({
      projectId,
      chains: [seismicTestnet],
      networks: [seismicTestnet],
    });
    
    createAppKit({
      adapters: [wagmiAdapter],
      projectId,
      networks: [seismicTestnet],
      metadata: {
        name: "My Seismic App",
        description: "A Seismic-powered dApp",
        url: "https://myapp.com",
        icons: ["https://myapp.com/icon.png"],
      },
    });
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider } from 'seismic-react'
    
    const queryClient = new QueryClient()
    
    function App({ children }: { children: React.ReactNode }) {
      return (
        <WagmiProvider config={wagmiAdapter.wagmiConfig}>
          <QueryClientProvider client={queryClient}>
            <ShieldedWalletProvider config={wagmiAdapter.wagmiConfig}>
              {children}
            </ShieldedWalletProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    function Header() {
      return (
        <header>
          <appkit-button />
        </header>
      )
    }
    import { useShieldedWallet } from 'seismic-react'
    
    function MyComponent() {
      const { walletClient, loaded, error } = useShieldedWallet()
    
      if (!loaded) return <div>Initializing shielded wallet...</div>
      if (error) return <div>Error: {error}</div>
      if (!walletClient) return <div>Connect your wallet to get started.</div>
    
      return <div>Shielded wallet ready!</div>
    }
    'use client'
    
    import { createAppKit } from '@reown/appkit/react'
    import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider, useShieldedWallet } from 'seismic-react'
    import { seismicTestnet } from 'seismic-react/rainbowkit'
    
    const projectId = 'YOUR_WALLETCONNECT_PROJECT_ID'
    
    const wagmiAdapter = new WagmiAdapter({
      projectId,
      chains: [seismicTestnet],
      networks: [seismicTestnet],
    })
    
    createAppKit({
      adapters: [wagmiAdapter],
      projectId,
      networks: [seismicTestnet],
      metadata: {
        name: 'My Seismic App',
        description: 'A Seismic-powered dApp',
        url: 'https://myapp.com',
        icons: ['https://myapp.com/icon.png'],
      },
    })
    
    const queryClient = new QueryClient()
    
    function WalletStatus() {
      const { walletClient, publicClient, loaded, error } = useShieldedWallet()
    
      if (!loaded) return <p>Initializing shielded wallet...</p>
      if (error) return <p>Error: {error}</p>
      if (!walletClient) return <p>Connect your wallet to get started.</p>
    
      return (
        <div>
          <p>Shielded wallet ready</p>
          <p>Public client: {publicClient ? 'Available' : 'Loading...'}</p>
        </div>
      )
    }
    
    export default function App() {
      return (
        <WagmiProvider config={wagmiAdapter.wagmiConfig}>
          <QueryClientProvider client={queryClient}>
            <ShieldedWalletProvider config={wagmiAdapter.wagmiConfig}>
              <appkit-button />
              <WalletStatus />
            </ShieldedWalletProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    WalletConnect Cloudarrow-up-right
    operations. All methods return coroutines that must be awaited. Use this class in async applications when you only need to read public contract data.

    hashtag
    Definition

    hashtag
    Constructor Parameters

    Parameter
    Type
    Required
    Description

    w3

    AsyncWeb3

    Yes

    Asynchronous AsyncWeb3 instance connected to RPC endpoint

    address

    ChecksumAddress

    hashtag
    Namespace

    hashtag
    .tread - Transparent Read

    Executes standard async eth_call with unencrypted calldata. This is the only namespace available on AsyncPublicContract.

    Returns: Coroutine[Any] (ABI-decoded Python value)

    Optional Parameters: None (pass positional arguments only)

    hashtag
    Examples

    hashtag
    Basic Read Operations

    hashtag
    Concurrent Reads

    hashtag
    Single and Multiple Returns

    hashtag
    Array Results

    hashtag
    Error Handling

    hashtag
    Notes

    • All methods return coroutines: Must use await with every .tread call

    • Read-only: No write operations available

    • No encryption required: Does not use EncryptionState or private keys

    • No authentication: Standard unsigned async eth_call operations

    • Gas not consumed: eth_call is free (doesn't create transactions)

    hashtag
    See Also

    • PublicContract - Synchronous version of this class

    • AsyncShieldedContract - Full async contract wrapper with write operations

    • create_async_public_client - Create async client without private key

    • - Overview of contract interaction patterns

    class AsyncPublicContract:
        def __init__(
            self,
            w3: AsyncWeb3,
            address: ChecksumAddress,
            abi: list[dict[str, Any]],
        ) -> None:
            ...
    import asyncio
    from seismic_web3 import create_async_public_client, AsyncPublicContract
    
    async def main():
        # Create async client without private key
        w3 = create_async_public_client(
            provider_url="https://testnet-1.seismictest.net/rpc",
        )
    
        # Create read-only contract instance
        contract = AsyncPublicContract(
            w3=w3,
            address="0x1234567890123456789012345678901234567890",
            abi=CONTRACT_ABI,
        )
    
        # Read public contract state (must await, auto-decoded)
        total_supply = await contract.tread.totalSupply()  # int
        print(f"Total supply: {total_supply}")
    
        balance = await contract.tread.balanceOf("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")  # int
        print(f"Balance: {balance}")
    
    asyncio.run(main())
    async def concurrent_reads(contract: AsyncPublicContract):
        # Execute multiple reads concurrently (all auto-decoded)
        total_supply, decimals, symbol, name = await asyncio.gather(
            contract.tread.totalSupply(),
            contract.tread.decimals(),
            contract.tread.symbol(),
            contract.tread.name(),
        )
    
        print(f"Name: {name}")
        print(f"Symbol: {symbol}")
        print(f"Decimals: {decimals}")
        print(f"Supply: {total_supply}")
    async def return_types(contract: AsyncPublicContract):
        # Single output values are returned directly
        number = await contract.tread.getNumber()    # int
        name = await contract.tread.getName()        # str
        active = await contract.tread.isActive()     # bool
    
        # Multiple outputs return a tuple
        user_name, balance, is_active = await contract.tread.getUserInfo(
            "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
        )
    async def array_example(contract: AsyncPublicContract):
        # Read array of addresses (auto-decoded to list)
        holders = await contract.tread.getHolders()
        print(f"Found {len(holders)} holders")
    
        # Query additional data for each holder concurrently
        balances = await asyncio.gather(
            *[contract.tread.balanceOf(holder) for holder in holders]
        )
    
        for holder, balance in zip(holders, balances):
            print(f"  {holder}: {balance}")
    async def error_handling(contract: AsyncPublicContract):
        try:
            value = await contract.tread.getNumber()
            print(f"Value: {value}")
    
        except ValueError as e:
            print(f"RPC error: {e}")
        except asyncio.TimeoutError:
            print("Request timed out")
        except Exception as e:
            print(f"Unexpected error: {e}")

    Intelligence Contracts

    Add compliance-compatible access control to your private token

    Privacy and compliance are often framed as opposites. Intelligence Contracts show that they are not. This chapter adds role-based access control to the SRC20 so that authorized parties -- auditors, compliance officers, regulators -- can inspect shielded state when required, without compromising privacy for everyone else. Estimated time: ~15 minutes.

    hashtag
    The concept

    An Intelligence Contract is a smart contract that can selectively reveal shielded state to authorized parties. The contract stores data privately by default, but includes gated functions that cast shielded values to their unshielded counterparts -- only for callers who hold the right role.

    The key insight: the data stays shielded in storage at all times. No plaintext balances are ever written to public state. Authorized parties read the data through signed reads, which means the response is encrypted to their key. The balance is revealed only to the specific authorized caller, not to the world.

    hashtag
    Why this matters

    Real-world token issuers need to answer questions like:

    • Can a compliance officer verify that an account's balance is below a threshold?

    • Can an auditor check aggregate balances across a set of accounts?

    • Can the token issuer freeze a specific account if required by law?

    Without Intelligence Contracts, privacy is all-or-nothing: either everyone can see everything, or no one can. With Intelligence Contracts, you get selective disclosure -- the right people see the right data, and no one else does.

    hashtag
    Implementation with AccessControl

    We will use OpenZeppelin's AccessControl to manage roles. This is a battle-tested pattern used across thousands of Ethereum contracts.

    hashtag
    Access tiers

    The contract implements three levels of access:

    Role
    Can do
    How they access

    hashtag
    Granting roles

    The deployer holds DEFAULT_ADMIN_ROLE and can grant roles to other addresses:

    In TypeScript:

    hashtag
    Compliance officer reading a balance

    The compliance officer uses a signed read, just like a regular user. The only difference is the function they call:

    The balance is returned encrypted to the compliance officer's key. No one else -- not even other compliance officers -- can see this specific response.

    hashtag
    The privacy guarantee

    Even with compliance roles in place, the privacy model is strong:

    1. Data stays shielded in storage. The balanceOf mapping always stores suint256. No plaintext balances are ever written to public state, regardless of who has what role.

    2. Reads go through signed reads. Whether it is a user checking their own balance or a compliance officer auditing an account, the query is a signed read. The response is encrypted to the caller's key.

    3. No broadcast disclosure.

    circle-info

    The role structure shown here is a starting point. In production, you might add time-limited roles, multi-sig requirements for granting compliance access, or on-chain audit logs that record when a compliance officer accessed a balance (without revealing the balance itself).

    Building the Frontend

    Connect your SRC20 contract to a React frontend with seismic-react

    This chapter connects the SRC20 contract to a React frontend using seismic-react, which composes with wagmiarrow-up-right to provide shielded reads, shielded writes, and encrypted communication out of the box. Estimated time: ~25 minutes.

    hashtag
    Overview

    By the end of this chapter you will have a React application that can:

    • Connect a wallet through a ShieldedWalletProvider

    • Display the user's shielded balance (via signed reads)

    • Transfer tokens (via shielded writes)

    • Listen for Transfer events and decrypt the encrypted amounts

    The patterns here mirror standard wagmi usage. If you have built a dApp with wagmi before, the seismic-react equivalents will feel familiar.

    hashtag
    Setup

    Install the required packages:

    hashtag
    Configure the ShieldedWalletProvider

    The ShieldedWalletProvider wraps your application and provides the shielded wallet context to all child components. It works alongside wagmi's standard provider:

    The ShieldedWalletProvider handles the cryptographic setup needed for Seismic transactions -- deriving encryption keys, managing the TEE public key, and signing shielded requests.

    hashtag
    Connecting to the contract

    Use the useShieldedContract hook to get a contract instance that supports shielded reads and writes:

    This hook returns a contract instance bound to the currently connected shielded wallet. All reads are signed reads and all writes are shielded writes.

    hashtag
    Reading balance (signed read)

    Use the useSignedReadContract hook to query the user's balance. This sends a signed read under the hood, so the contract can verify msg.sender and the response is encrypted:

    The useSignedReadContract hook handles the full signed-read flow: signing the request with the user's key, sending it to eth_call, and decrypting the encrypted response. It returns a signedRead function that you call imperatively to perform the read.

    hashtag
    Transferring tokens (shielded write)

    Use the useShieldedWriteContract hook to send a shielded transfer. The calldata is encrypted before leaving the user's machine:

    When transfer() is called, the seismic-react library:

    1. Fetches the TEE public key from the node.

    2. Derives a shared secret via ECDH.

    3. Encrypts the calldata (including the recipient and amount) with AEAD.

    4. Wraps everything in a Seismic transaction (type 0x4A

    The user sees a standard wallet confirmation prompt. The privacy happens automatically under the hood.

    hashtag
    Decrypting events

    If your contract uses encrypted events (from the chapter), you can listen for them and decrypt the amounts off-chain:

    Decrypt events client-side using the ECDH shared secret and AES-GCM decryption. See the for the cryptographic primitives.

    hashtag
    Complete example

    Here is the full dashboard component that ties everything together:

    circle-info

    In a production application, you should not hardcode private keys. Use a wallet provider (such as RainbowKit, Privy, or AppKit) to manage keys securely. See the for integration details.

    hashtag
    Next steps

    You now have a complete SRC20 token: a private ERC20 with shielded balances, encrypted events, signed reads, compliance access control, and a React frontend.

    From here, you can:

    • Explore the client library docs -- The has detailed API references for seismic-viem and seismic-react, including all available hooks, wallet client methods, and precompile utilities.

    • Add wallet integration -- See the for step-by-step instructions on integrating RainbowKit, Privy, or AppKit with seismic-react.

    Events

    hashtag
    The Limitation

    Shielded types cannot be emitted directly in events. The following will not compile:

    event ConfidentialEvent(suint256 confidentialData); // Compilation error

    This restriction exists because events are stored in transaction logs, which are publicly accessible on-chain. Emitting a shielded value in an event would defeat the purpose of shielding it in the first place -- the value would be visible to anyone inspecting the logs.

    This applies to all shielded types: suint, sint, sbool, saddress, and sbytes.

    hashtag
    The Workaround: Encrypted Events via Precompiles

    Although native encrypted events are not yet supported, you can achieve private event data today using the AES-GCM and ECDH precompiles. The approach is to encrypt the sensitive data before emitting it in a regular (unshielded) event, so that only the intended recipient can decrypt it.

    Here is the general flow:

    1. Generate a shared secret between the sender and the intended recipient using the at address 0x65.

    2. Derive an encryption key from the shared secret using the at address 0x68.

    3. Encrypt the event data using the at address

    hashtag
    Precompile Reference

    Precompile
    Address
    Purpose

    hashtag
    Code Example

    Below is a minimal private token showing how to emit encrypted transfer events. The contract holds a keypair; users register their public keys so the contract can encrypt event data that only the recipient can read.

    For a full implementation, see the tutorial.

    The built-in helpers ecdh(), hkdf(), and aes_gcm_encrypt() are compiler-provided globals — no imports needed. See the for details on each.

    hashtag
    Decryption (Off-Chain)

    The recipient reconstructs the shared secret off-chain using their own private key and the contract's public key (ECDH is symmetric). They then derive the same encryption key via HKDF and decrypt the event data using AES-GCM Decrypt.

    The client libraries provide built-in helpers for this:

    • — watch_src20_events_with_key and SRC20EventWatcher

    • — watchSRC20EventsWithKey() action

    hashtag
    What Not to Do

    Do not attempt to work around the restriction by casting a shielded value to its unshielded counterpart and then emitting it:

    Casting from a shielded type to an unshielded type makes the value visible in the execution trace. Emitting it in an event then permanently records it in publicly accessible logs.

    hashtag
    Future Improvements

    Native encrypted events are planned for a future version of Seismic. This will allow shielded types to be emitted directly in events without requiring manual encryption via precompiles. The compiler and runtime will handle encryption transparently, making private events as simple to use as regular events.

    hashtag
    Key Takeaway

    Privacy in events is achievable today -- it just requires explicit encryption via the aforementioned precompiles. Encrypt sensitive data before emitting it, and ensure only the intended recipient has the keys to decrypt. When native encrypted events ship, migrating will be straightforward: replace the manual encryption logic with direct shielded type emission.

    Ch 3: Rounds and Contributor Access

    In this chapter, we'll implement a reset mechanism that allows the clown to get back up for multiple rounds, ensuring each game session starts fresh. We'll also track contributors per round so that only players who participated in knocking out the clown can call rob(). By the end, we'll have a fully functional round-based game where secrets remain shielded until conditions are met! Estimated time: ~15 minutes.

    hashtag
    The need for a Reset mechanism

    Right now, once the clown is knocked out, there's no way to reset it. If a game session were to continue, we'd have no way to start fresh — the stamina would remain at 0, and the secret would be permanently accessible.

    To solve this, we need to introduce:

    • A reset function that restores the clown to its original state.

    • Round tracking, so each reset creates a new round.

    hashtag
    The need for a contributor check

    While the reset mechanism and round tracking allow us to restart the game for continuous play, they still don't address who should be allowed to call the rob() function.

    Right now, any player can call rob() once the clown is knocked out, even if they didn't participate in hitting it during the current round. This creates the following issues:

    • Fairness: Players who didn't contribute should not be able to reap the benefits of robbing the clown.

    • Incentivizing Contribution: The game needs to encourage active participation by ensuring that only those who helped knock out the clown in a specific round are rewarded with access to the secret.

    The solution to this is implementing a conditional check on rob() which allows only those players who contributed at least one hit in the current round to steal the secret.

    hashtag
    Implementing the Reset Mechanism

    The reset mechanism allows the clown to get back up for multiple rounds, with each round starting fresh. It restores the clown's stamina and picks a new random secret, then increments the round counter.

    Here's how we can implement the reset function:

    What's Happening Here?

    • Condition for Reset (requireDown): The reset function can only be called once the clown is knocked out, enforced by the requireDown modifier.

    • Restoring Initial State: The stamina is reset to initialClownStamina, and a new random secret is selected via _randomIndex().

    hashtag
    Modifying hit() to track contributions

    To enforce fair access to the secrets, we'll track the number of hits each player contributes in a given round. This is achieved using the hitsPerRound mapping:

    Every time a player calls the hit() function, we update their contribution in the current round:

    What's Happening Here?

    • Tracking Contributions: The hitsPerRound mapping records each player's hits in the current round. This ensures we can verify who participated when the clown was knocked out.

    • Replayable Rounds: Because contributions are tracked by round, the game can fairly reset and start fresh without losing player data from previous rounds.

    hashtag
    Restricting rob() with a contributor check

    To ensure only contributors can rob the clown, we'll use a modifier called onlyContributor:

    We'll then apply this modifier to the rob() function:

    Congratulations! You made it through writing the entire shielded smart contract for a multiplayer, multi-round, Clown Beatdown game!

    Final ClownBeatdown contract

    Now, onto testing the contract!

    Installation

    Install seismic-react and configure peer dependencies

    hashtag
    Prerequisites

    Requirement
    Version
    Notes

    hashtag
    Install

    hashtag
    Peer Dependencies

    seismic-react requires several peer dependencies that your project must provide:

    Package
    Purpose

    Install all required peer dependencies:

    circle-info

    If you are using RainbowKit for wallet UI, also install @rainbow-me/rainbowkit:

    hashtag
    Wagmi Config Setup

    Create a wagmi config with Seismic chain definitions:

    circle-exclamation

    Replace YOUR_WALLETCONNECT_PROJECT_ID with a project ID from .

    hashtag
    Minimal Working App

    hashtag
    Package Exports

    seismic-react provides two entry points:

    Entry Point
    Contents

    hashtag
    TypeScript

    TypeScript >= 5.0.4 is an optional peer dependency. seismic-react ships with full type definitions and provides type inference from contract ABIs when using hooks like useShieldedWriteContract and useSignedReadContract.

    No additional @types/* packages are needed.

    hashtag
    Troubleshooting

    hashtag
    Module Not Found: seismic-viem

    Ensure seismic-viem is installed as a peer dependency:

    hashtag
    wagmi Version Mismatch

    seismic-react requires wagmi v2. If you have wagmi v1 installed, upgrade:

    hashtag
    RainbowKit Chain Not Appearing

    Make sure you import chain configs from the seismic-react/rainbowkit entry point, not from the main entry:

    hashtag
    React Version Conflicts

    If you encounter React version conflicts in a monorepo, ensure all packages resolve to the same React version. Add a resolutions field (Yarn) or overrides field (npm) to your root package.json:

    hashtag
    See Also

    • -- Provider setup and configuration

    • -- Available React hooks

    • -- Wallet UI integration

    seismic-react

    React hooks and providers for Seismic, composing with wagmi to add shielded wallet management, encrypted transactions, and signed reads to React apps.

    React SDK (v1.1.1) for Seismicarrow-up-right, built on wagmiarrow-up-right 2.0+ and viemarrow-up-right 2.x. Provides ShieldedWalletProvider context and hooks for encrypted transactions and signed reads in React applications.

    npm install seismic-react

    hashtag
    Quick Start

    Minimal setup with RainbowKit:

    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit'
    import { ShieldedWalletProvider } from 'seismic-react'
    import { seismicTestnet } from 'seismic-react/rainbowkit'
    
    const config = getDefaultConfig({
      appName: 'My Seismic App',
      projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
      chains: [seismicTestnet],
    })
    
    const queryClient = new QueryClient()
    
    function App() {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                <YourApp />
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }

    hashtag
    Architecture

    The SDK wraps wagmi's connector layer to inject Seismic's shielded wallet and public clients:

    hashtag
    Documentation Navigation

    hashtag
    Getting Started

    Section
    Description

    hashtag
    Hooks Reference

    Section
    Description

    hashtag
    Wallet Guides

    Section
    Description

    hashtag
    Quick Links

    hashtag
    By Task

    • Connect a shielded wallet ->

    • Read shielded state ->

    • Send an encrypted transaction ->

    hashtag
    By Component

    • Provider ->

    • Hooks -> , , ,

    • Chain configs -> seismicTestnet, sanvil, localSeismicDevnet

    hashtag
    Features

    • Shielded Transactions -- Encrypt calldata before sending via useShieldedWriteContract

    • Signed Reads -- Prove caller identity in eth_call with useSignedReadContract

    hashtag
    Next Steps

    1. -- Add the package and peer dependencies

    2. -- Wrap your app with the provider

    3. -- Choose a wallet integration

    Basic dApp

    Complete minimal dApp with shielded writes and signed reads

    This example builds a complete minimal dApp that connects a wallet, sends a shielded write transaction, and performs a signed read -- all using seismic-react hooks.

    hashtag
    What You'll Build

    1. Provider setup with RainbowKit + Seismic

    2. Wallet connection via RainbowKit's ConnectButton

    3. Shielded write to a contract (encrypted calldata)

    4. Signed read from a contract (authenticated query)

    hashtag
    Prerequisites

    • Node.js 18+

    • A project ID

    Install dependencies:

    hashtag
    Step 1: wagmi Config

    Create the wagmi configuration with the Seismic testnet chain:

    circle-exclamation

    Replace YOUR_WALLETCONNECT_PROJECT_ID with your actual project ID from .

    hashtag
    Step 2: Provider Wrapper

    Wrap your app with the required providers. ShieldedWalletProvider must be nested inside the wagmi and React Query providers:

    hashtag
    Step 3: Contract Interaction Component

    Create a component that uses useShieldedWriteContract and useSignedReadContract to interact with a shielded counter contract:

    circle-info

    Replace CONTRACT_ADDRESS with the address of a deployed shielded contract on Seismic testnet. The ABI above is for a simple counter -- adapt it to match your contract.

    hashtag
    Step 4: App Component

    Combine the wallet connection button with the counter component. The counter only renders once the shielded wallet is ready:

    hashtag
    What's Happening

    1. RainbowKit handles wallet connection and chain switching

    2. ShieldedWalletProvider automatically creates shielded clients when a wallet connects, performing the ECDH key exchange with the TEE

    3. useShieldedWriteContract encrypts calldata before sending the transaction, ensuring on-chain privacy

    hashtag
    Next Steps

    • - Full API for all hooks

    • - Use AppKit or Privy instead of RainbowKit

    • - Configure Seismic testnet or local Sanvil

    hashtag
    See Also

    • - Provider configuration options

    • - Shielded write hook API

    • - Signed read hook API

    useShieldedRead

    Execute authenticated read calls on shielded contracts

    circle-info

    This hook wraps useSignedReadContract from seismic-react. The actual export name is useSignedReadContract.

    Hook for performing signed reads -- authenticated eth_call requests where the caller proves their identity. This allows contracts to return caller-specific shielded data (for example, a balance that depends on msg.sender).

    import { useSignedReadContract } from "seismic-react";

    hashtag
    Config

    Parameter
    Type
    Required
    Description

    hashtag
    Return Type

    Property
    Type
    Description

    hashtag
    Usage

    hashtag
    Reading a shielded balance

    hashtag
    Loading state handling

    hashtag
    Error handling

    circle-info

    Unlike wagmi's useReadContract which auto-fetches on mount, useSignedReadContract returns a function you call imperatively. This is because signed reads require wallet interaction to prove caller identity.

    hashtag
    See Also

    • -- Send encrypted write transactions

    • -- Contract instance with both read and write methods

    • -- Access the underlying wallet client

    RainbowKit

    Set up RainbowKit with Seismic for wallet connection

    RainbowKit is the recommended wallet connection library for Seismic React apps. It provides a polished connect modal, chain switching, and account management out of the box.

    hashtag
    Prerequisites

    npm install @rainbow-me/rainbowkit wagmi viem @tanstack/react-query seismic-react seismic-viem

    hashtag
    Step 1: Configure wagmi

    Create a wagmi config using RainbowKit's getDefaultConfig with Seismic chains:

    circle-exclamation

    Replace YOUR_WALLETCONNECT_PROJECT_ID with a project ID from .

    hashtag
    Step 2: Set Up Providers

    Wrap your app with the provider stack. The nesting order matters:

    circle-info

    ShieldedWalletProvider must be inside RainbowKitProvider so it can access the connected wallet from wagmi's context.

    hashtag
    Step 3: Add the Connect Button

    RainbowKit provides a pre-built connect button component:

    hashtag
    Step 4: Use Shielded Hooks

    Once the providers are in place, use seismic-react hooks to interact with shielded contracts:

    hashtag
    Next.js Setup

    Next.js App Router requires client components for providers. Create a separate providers file:

    Then wrap your root layout:

    circle-info

    The 'use client' directive is required because providers use React context, which is only available in client components.

    hashtag
    Local Development with Sanvil

    For local development, use the sanvil chain (Seismic's local development node) instead of seismicTestnet:

    hashtag
    Complete Example

    A full working setup in a single file:

    hashtag
    See Also

    • -- Comparison of wallet libraries

    • -- Provider reference and options

    • -- Access shielded wallet context

    Python — seismic-web3

    Python SDK for Seismic, built on web3.py

    Python SDK for Seismicarrow-up-right, built on web3.pyarrow-up-right. Requires Python 3.10+.

    pip install seismic-web3

    Or with uvarrow-up-right:

    uv add seismic-web3

    hashtag
    Quick Example

    import os
    from seismic_web3 import SEISMIC_TESTNET, PrivateKey
    
    pk = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
    # Wallet client — full capabilities (requires private key)
    w3 = SEISMIC_TESTNET.wallet_client(pk)
    
    contract = w3.seismic.contract(address="0x...", abi=ABI)
    
    # Shielded write — calldata is encrypted
    tx_hash = contract.write.setNumber(42)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    
    # Signed read — encrypted eth_call, proves your identity
    result = contract.read.getNumber()
    # Public client — read-only (no private key needed)
    public = SEISMIC_TESTNET.public_client()
    
    contract = public.seismic.contract(address="0x...", abi=ABI)
    result = contract.tread.getNumber()

    hashtag
    Documentation Navigation

    hashtag
    Getting Started

    Section
    Description

    hashtag
    Guides

    Section
    Description

    hashtag
    API Reference

    Section
    Description

    hashtag
    Advanced Features

    Section
    Description

    hashtag
    Quick Links

    hashtag
    By Task

    • Send a shielded transaction →

    • Execute a signed read →

    • Work with SRC20 tokens →

    hashtag
    By Component

    • Client factories → ,

    • Contract wrappers → ,

    • Encryption → ,

    hashtag
    Features

    • 🔒 Shielded Transactions - Encrypt calldata with TEE public key

    • 📝 Signed Reads - Prove identity in eth_call

    • 🪙 SRC20 Support - Built-in support for private tokens

    hashtag
    Architecture

    The SDK extends web3.py with a custom w3.seismic namespace:

    hashtag
    Next Steps

    1. - Get connected to Seismic

    2. - Step-by-step guide

    3. - Deep dive into all types and functions

    Client

    Creating sync and async Seismic clients

    The Seismic Python SDK provides two client types for interacting with Seismic nodes:

    • Wallet client — Full capabilities (shielded writes, signed reads, deposits). Requires a private key.

    • Public client — Read-only (transparent reads, TEE public key, deposit queries). No private key needed.

    Both clients are available in sync and async variants.

    hashtag
    Quick Start

    hashtag
    Installation

    Or with :

    hashtag
    Wallet Client (Sync)

    This gives you a standard Web3 instance with an extra w3.seismic namespace. Everything from web3.py works as usual — w3.eth.get_block(...), w3.eth.wait_for_transaction_receipt(...), etc.

    hashtag
    Wallet Client (Async)

    Every method on w3.seismic and on ShieldedContract is await-able when using the async client.

    hashtag
    Public Client

    The public client's w3.seismic namespace has limited methods: get_tee_public_key(), get_deposit_root(), get_deposit_count(), and contract() (with .tread only).

    hashtag
    Client Factory Functions

    hashtag
    Wallet Clients (Require Private Key)

    Function
    Type
    Description

    hashtag
    Public Clients (No Private Key)

    Function
    Type
    Description

    hashtag
    Chain-Based Creation

    The recommended approach is to use chain configuration objects:

    See for more details.

    hashtag
    Encryption

    The wallet client automatically handles encryption setup:

    1. Fetches the network's TEE public key

    2. Derives a shared key via

    3. Uses this key to encrypt calldata for all shielded transactions and signed reads

    You don't need to manage this manually, but the encryption state is accessible at w3.seismic.encryption if needed.

    hashtag
    Encryption Components

    Component
    Description

    hashtag
    Client Capabilities

    hashtag
    Wallet Client (w3.seismic)

    • - Send shielded transactions

    • - Debug shielded transactions

    • - Execute signed reads

    hashtag
    Public Client (w3.seismic)

    • - Get TEE public key

    • - Query deposit merkle root

    • - Query deposit count

    hashtag
    See Also

    • - Working with shielded and public contracts

    • - Detailed w3.seismic namespace documentation

    • - Chain configs and constants

    useShieldedContract

    Get a ShieldedContract instance for reads and writes

    Hook that creates a ShieldedContract instance from seismic-viem's getShieldedContract. Provides a contract object with type-safe methods for both shielded writes and signed reads.

    import { useShieldedContract } from "seismic-react";

    hashtag
    Config

    Parameter
    Type
    Required
    Description

    hashtag
    Return Type

    Property
    Type
    Description

    hashtag
    Usage

    hashtag
    Basic

    hashtag
    Using the contract for reads and writes

    Once you have a ShieldedContract instance, call its methods directly for both signed reads and shielded writes:

    hashtag
    TypeScript ABI typing

    For full type inference on contract methods, define your ABI with as const:

    circle-info

    useShieldedContract requires a connected wallet via ShieldedWalletProvider. The contract field is null until the wallet client is initialized.

    hashtag
    See Also

    • -- Access the underlying wallet and public clients

    • -- Send encrypted writes without a contract instance

    • -- Perform signed reads without a contract instance

    Ch 1: Project Setup and Providers

    In this chapter, you'll set up the React frontend project and configure the provider stack that enables shielded wallet interactions in the browser. Estimated time: ~15 minutes

    hashtag
    Creating the web package

    From the root of your clown-beatdown monorepo, create a new Vite + React + TypeScript project:

    Footguns

    Shielded types protect values at rest and in transit, but careless usage patterns can leak information through side channels. This page covers the most common information leak vectors when working with shielded types.

    hashtag
    Conditional Execution

    The problem: Using an sbool in a conditional branch leaks information through the execution trace and gas consumption. Observers can tell which branch was taken by examining the gas used or the operations performed.

    Why it leaks: The EVM execution trace shows which opcodes were executed. If the

    useShieldedWrite

    Send encrypted write transactions to shielded contracts

    circle-info

    This hook wraps useShieldedWriteContract from seismic-react. The actual export name is useShieldedWriteContract.

    Hook for sending shielded write transactions. Encrypts calldata before submission so transaction data is not visible on-chain.

    Shielded Literals

    The s suffix lets you write shielded integer constants directly, without explicit casting. The compiler infers the concrete shielded type (suint8, suint256, sint128, etc.) from context.

    This is equivalent to:

    Both forms are valid. The s suffix is syntactic sugar — it produces the same bytecode.

    Yes

    Validator BLS12-381 consensus public key

    withdrawalCredentials

    Hex

    Yes

    Commitment to a public key for future withdrawals

    nodeSignature

    Hex

    Yes

    ED25519 signature over the deposit data

    consensusSignature

    Hex

    Yes

    BLS12-381 signature over the deposit data

    depositDataRoot

    Hex

    Yes

    SHA-256 hash of the SSZ-encoded DepositData -- acts as a checksum

    value

    bigint

    Yes

    Amount to deposit in wei

    address

    Address

    No

    Deposit contract address (defaults to the canonical)

    RainbowKit Guide
    Privy Guide

    Yes

    Contract address (checksummed Ethereum address)

    abi

    list[dict[str, Any]]

    Yes

    Contract ABI (list of function entries)

    Contract Instance Guide

    View any balance, freeze/unfreeze accounts

    complianceBalanceOf(account) via signed read; complianceFreeze(account) via transaction

    When a compliance officer reads a balance, only they learn the value. It is not published on-chain or visible to other observers.
  • Roles are on-chain and auditable. The AccessControl roles are standard Solidity state. Anyone can verify who holds what role by reading the contract. The role assignments themselves are transparent -- only the shielded data they gate is private.

  • Freeze is public. The frozen mapping uses bool, not sbool. This is a deliberate design choice: if an account is frozen, that fact should be publicly verifiable so that counterparties know not to send tokens to it.

  • Regular user

    View their own balance

    getBalance(myAddress) via signed read

    Auditor (AUDITOR_ROLE)

    View any account's balance (read-only)

    auditBalanceOf(account) via signed read

    Compliance officer (COMPLIANCE_ROLE)

    ).
  • Broadcasts the encrypted transaction.

  • Deploy to testnet -- See the deploy section for deploying your SRC20 to a live Seismic network.
  • Extend the contract -- Consider adding features like burn functions or governance mechanisms.

  • Encrypted Events
    seismic-viem precompiles documentation
    Wallet Guides
    Client Libraries section
    Wallet Guides

    Round Tracking: The round counter increments each time the clown is reset, allowing us to distinguish between rounds.

    Seismic transport layer providing ShieldedPublicClient and ShieldedWalletClient

    @tanstack/react-query

    Async state management (required by wagmi)

    -- Embedded wallet setup

    Node.js

    18+

    LTS recommended

    React

    ^18

    Peer dependency

    wagmi

    ^2.0.0

    Peer dependency

    viem

    2.x

    Peer dependency

    seismic-viem

    >=1.1.1

    Seismic transport layer

    @rainbow-me/rainbowkit

    ^2.0.0

    Optional, for wallet UI

    react

    React runtime

    wagmi

    Ethereum React hooks and wallet connectors

    viem

    TypeScript Ethereum library (used internally by wagmi)

    seismic-react

    Main entry -- ShieldedWalletProvider, useShieldedWallet, useShieldedContract, useShieldedWriteContract, useSignedReadContract

    seismic-react/rainbowkit

    Chain configs -- seismicTestnet, sanvil, localSeismicDevnet, createSeismicDevnet

    WalletConnect Cloudarrow-up-right
    ShieldedWalletProvider
    Hooks Overview
    RainbowKit Guide

    seismic-viem

    Privy Guide
    useSignedReadContract attaches a signature proving caller identity, allowing the contract to return private data only to authorized readers
    - Full dependency setup
    WalletConnectarrow-up-right
    WalletConnect Cloudarrow-up-right
    Hooks Reference
    Wallet Guides
    Chains
    ShieldedWalletProvider
    useShieldedWriteContract
    useSignedReadContract
    Installation

    Contract ABI

    functionName

    string

    Yes

    Name of the view/pure function to call

    args

    array

    No

    Arguments to pass to the function

    boolean

    Whether a read is in progress

    error

    Error | null

    Error from the most recent read

    ShieldedWalletProvider -- Context provider required by this hook
  • Hooks Overview -- Summary of all hooks

  • address

    `0x${string}`

    Yes

    Contract address

    abi

    Abi

    signedRead

    () => Promise<any>

    Function to execute the signed read

    read

    () => Promise<any>

    Alias for signedRead

    useShieldedWriteContract
    useShieldedContract
    useShieldedWallet

    Yes

    isLoading

    -- Email/social login alternative
  • AppKit Guide -- WalletConnect modal alternative

  • WalletConnect Cloudarrow-up-right
    Wallet Guides Overview
    ShieldedWalletProvider
    useShieldedWallet
    Privy Guide
    - Get TEE public key
  • deposit() - Deposit ETH/tokens

  • get_deposit_root() - Query deposit merkle root

  • get_deposit_count() - Query deposit count

  • contract() - Create contract wrappers

  • - Create contract wrappers (
    .tread
    only)
    Shielded Write Guide - Step-by-step shielded transaction guide

    create_wallet_client

    Sync

    Create sync wallet client from RPC URL

    create_async_wallet_client

    Async

    Create async wallet client from RPC URL

    create_public_client

    Sync

    Create sync public client from RPC URL

    create_async_public_client

    Async

    Create async public client from RPC URL

    EncryptionState

    Holds AES key and encryption keypair

    get_encryption

    Derives encryption state from TEE public key

    uvarrow-up-right
    Chains Configuration
    AES-GCMarrow-up-right
    ECDHarrow-up-right
    send_shielded_transaction()
    debug_send_shielded_transaction()
    signed_call()
    get_tee_public_key()
    get_deposit_root()
    get_deposit_count()
    contract()
    Contract Instances
    Namespaces
    Chains Configuration
    get_tee_public_key()

    Address

    The address passed in

    error

    Error | null

    Error if wallet client not initialized

    ShieldedWalletProvider -- Context provider required by this hook
  • Hooks Overview -- Summary of all hooks

  • abi

    Abi

    Yes

    Contract ABI

    address

    Address

    Yes

    Contract address

    contract

    ShieldedContract | null

    The shielded contract instance

    abi

    Abi

    The ABI passed in

    useShieldedWallet
    useShieldedWriteContract
    useSignedReadContract

    address

    hashtag
    Install dependencies

    Install the core dependencies for wallet connection, shielded interactions, UI, and animations:

    The Vite config below uses the SWC plugin for faster builds. Install it as a dev dependency:

    hashtag
    Copy public assets

    Copy the public/ folder from the seismic-starterarrow-up-right repo into packages/web/public/. This includes the clown sprites, button images, background, logo, and audio files used by the game UI.

    hashtag
    Configure Vite

    Update vite.config.ts:

    The envDir points to the monorepo root so that .env files at the top level are available to the web package.

    hashtag
    Environment variables

    Create a .env file at the monorepo root:

    VITE_CHAIN_ID determines which chain the app connects to — 31337 is the local sanvil node.

    hashtag
    Setting up the provider stack

    The key architectural pattern in a seismic-react app is the provider stack. This wraps your application in the context providers needed for wallet connection and shielded operations.

    Create src/App.tsx:

    hashtag
    What's happening here?

    The provider stack nests four layers, each adding functionality:

    1. WagmiProvider — manages wallet connections and chain state via wagmi hooks (useAccount, useConnect, etc.)

    2. QueryClientProvider — provides React Query for caching and background data fetching

    3. RainbowKitProvider — adds a polished wallet connect modal UI

    4. ShieldedWalletProvider — the Seismic-specific layer from seismic-react that derives a shielded wallet client from the connected wagmi account, enabling shielded reads and writes. It takes config and options — the options include publicTransport, publicChain, and an onAddressChange callback.

    The onAddressChange handler auto-funds new wallets when running on sanvil (local dev), so you don't need to manually send ETH to test accounts.

    hashtag
    Supporting files

    Before wiring up the entry point, create the supporting modules that main.tsx and App.tsx will import.

    Redux store — Create src/store/store.ts:

    MUI theme — Create src/theme.ts:

    Page components — Create src/pages/Home.tsx:

    Create src/pages/NotFound.tsx:

    Stylesheets — Create src/App.css (empty for now) and replace src/index.css with:

    hashtag
    Entry point: main.tsx

    Create src/main.tsx to bootstrap the app with theme and state management:

    if
    branch runs different code (or a different amount of code) than the
    else
    branch, observers can determine which branch was taken, revealing the value of the shielded boolean.

    What to do instead: Ensure both branches execute the same operations with the same gas cost. A ternary where both arms are simple assignments of the same type is safe — the EVM does the same work for both paths.

    hashtag
    Literals

    The problem: Assigning literal values to shielded types — whether via explicit cast or the s suffix — embeds those values directly in the contract bytecode, which is publicly visible.

    What to do instead: Be aware that literals are embedded in contract bytecode and are publicly visible. The compiler emits warning 9660 for all shielded literals to remind you. If the initial value is sensitive, introduce it via encrypted calldata instead of hardcoding it.

    hashtag
    Dynamic Loops

    The problem: Using a shielded value as a loop bound leaks the value through gas consumption. Each iteration costs gas, so the total gas used reveals how many times the loop executed.

    Why it leaks: Gas is publicly visible. If a loop runs 5 times vs. 100 times, the gas difference is observable. This reveals the shielded loop bound.

    What to do instead: Use a fixed-size loop with a known maximum, and perform no-op iterations when the actual count is smaller. The no-op path must cost the same gas as the real-work path — otherwise an observer can count how many iterations did real work by comparing per-iteration gas costs.

    hashtag
    Unprotected View Functions

    The problem: If you write a view function that unshields and returns private data without access control, anyone can read it. The shielded storage is meaningless if a public getter exposes the plaintext.

    Why it leaks: The function casts the shielded value to a plain uint256 and returns it with no restriction on who can call it. The value is returned in plaintext to the caller.

    What to do instead: Always add access control to view functions that unshield data. If the getter checks msg.sender, callers must use a signed read — otherwise msg.sender will be the zero address.

    circle-info

    Access control doesn't have to be sender-based. Time-locked reveals, role-based access, or any other gating logic is fine — the important thing is that the function doesn't unconditionally return private data.

    hashtag
    Public Shielded Variables

    The problem: Declaring a shielded variable as public will not compile. Solidity automatically generates a public getter for public state variables, which would return the shielded value -- violating the rule that shielded types cannot be returned from public or external functions.

    What to do instead: Declare shielded variables as private or internal. If you need to expose the value, unshield it explicitly with a cast, and use access control (see above).

    hashtag
    Unencrypted Calldata

    The problem: There is currently nothing enforcing that functions with shielded parameters are called via a Seismic transaction (type 0x4A). You can call a function that accepts suint256 with a regular transaction, and the shielded parameter values will be visible in plaintext in the transaction's input data. Likewise, you can use a Seismic transaction to call a function with no shielded inputs — the encryption is unnecessary but harmless.

    What to do instead: If a function has any shielded inputs, always call it via a Seismic transaction. This is the caller's responsibility — the contract cannot currently enforce it. We plan to tighten this up in the future so the runtime rejects non-Seismic calls to functions with shielded parameters.

    hashtag
    Exponentiation

    The problem: The ** operator has a gas cost that scales with the value of the exponent. If the exponent is a shielded value, the gas cost reveals the exponent.

    Why it leaks: The EVM's modular exponentiation implementation uses more gas for larger exponents. An observer monitoring gas consumption can estimate the exponent value.

    What to do instead: Avoid using shielded values as exponents entirely. If you need exponentiation with a private exponent, consider alternative algorithms and make sure you think carefully about their gas consumption profile.

    hashtag
    Enums

    The problem: Enum types have a small, known range of possible values. If you convert an enum to a shielded type, the limited range makes it easy to guess the shielded value — an observer only needs to try a handful of possibilities.

    What to do instead: Consider whether the limited range of possible values undermines the privacy guarantee. Shielding an enum with 3 members is not meaningfully private.

    hashtag
    immutable and constant Shielded Variables

    Shielded types cannot be declared as immutable or constant. The compiler will reject both with an error. Constants are embedded in bytecode (publicly visible), and immutables are stored in bytecode after construction — neither is compatible with the confidential storage model.

    What to do instead: Use a regular private shielded variable initialized in the constructor or via a setter function called through a Seismic transaction.

    hashtag
    RNG Proposer Bias

    The problem: The synchronous RNG precompile produces randomness that is deterministic given the enclave's secret key, the transaction hash, remaining gas, and personalization bytes. In theory, a block proposer could simulate RNG outputs and selectively include, exclude, or reorder transactions to influence outcomes.

    Why this matters: Seismic's TEE setup largely mitigates this — proposers are restricted in what they can observe and do, and we believe synchronous RNG is safe for most use cases. This is something we've thought about extensively. However, for applications with especially high-stakes randomness requirements, it's worth being aware of the theoretical attack surface.

    What to do instead: For the most sensitive randomness use cases (large-pot lotteries, leader elections), consider using an asynchronous commit-reveal scheme where entropy is committed before the block in which it is consumed. Seismic does not provide this out of the box today. For the vast majority of use cases, the synchronous RNG precompile is appropriate.

    hashtag
    RNG Revert If Not Ideal Outcome

    The problem: A caller of a contract that uses the RNG precompile can revert their transaction if they don't like the outcome, giving them a risk-free way of interacting with your contract.

    Why this matters: You should not use RNG precompile when the caller will be able to analayze the outcome and revert to undo

    What to do instead: One example would be a poker contract where a dealer makes a call that uses RNG to shuffle the deck and set the state of it. After deck is shuffled players then can put in subsequent calls to deal from the shuffled deck but now they cannot revert to undo the state of the deck.

    hashtag
    Supported formats

    The s suffix works with all numeric literal forms:

    hashtag
    Type inference

    The shielded type is inferred from the assignment target or surrounding expression:

    hashtag
    Rules

    No mixed arithmetic. You cannot mix shielded and non-shielded literals in the same expression:

    No implicit conversion to non-shielded types. A shielded literal cannot be assigned to a regular type:

    No immutable or constant. Shielded literals follow the same restriction as all shielded types — they cannot be used in immutable or constant declarations. See Footguns.

    hashtag
    Compiler warning

    All shielded literals emit warning 9660. This is intentional — literal values are embedded in the contract bytecode, which is publicly visible. The warning reminds you that the value is leaked at deployment time.

    If the literal value is sensitive, do not hardcode it. Introduce it via encrypted calldata instead. See Footguns for more detail.

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.13;
    
    import "@openzeppelin/contracts/access/AccessControl.sol";
    
    contract SRC20 is AccessControl {
        string public name;
        string public symbol;
        uint8 public decimals = 18;
        uint256 public totalSupply;
    
        mapping(address => suint256) balanceOf;
        mapping(address => mapping(address => suint256)) allowance;
        mapping(address => bool) public frozen;
    
        bytes32 public constant COMPLIANCE_ROLE = keccak256("COMPLIANCE_ROLE");
        bytes32 public constant AUDITOR_ROLE = keccak256("AUDITOR_ROLE");
    
        event Transfer(address indexed from, address indexed to, uint256 amount);
        event Approval(address indexed owner, address indexed spender, uint256 amount);
        event AccountFrozen(address indexed account);
        event AccountUnfrozen(address indexed account);
    
        constructor(string memory _name, string memory _symbol, uint256 _initialSupply) {
            name = _name;
            symbol = _symbol;
            totalSupply = _initialSupply;
            balanceOf[msg.sender] = suint256(_initialSupply);
    
            // Deployer gets admin role and can grant other roles
            _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
        }
    
        // --- Standard token functions (with freeze check) ---
    
        function transfer(address to, suint256 amount) public returns (bool) {
            require(!frozen[msg.sender], "Account frozen");
            require(!frozen[to], "Recipient frozen");
            require(balanceOf[msg.sender] >= amount, "Insufficient balance");
    
            balanceOf[msg.sender] -= amount;
            balanceOf[to] += amount;
            emit Transfer(msg.sender, to, uint256(amount));
            return true;
        }
    
        function approve(address spender, suint256 amount) public returns (bool) {
            require(!frozen[msg.sender], "Account frozen");
            allowance[msg.sender][spender] = amount;
            emit Approval(msg.sender, spender, uint256(amount));
            return true;
        }
    
        function transferFrom(address from, address to, suint256 amount) public returns (bool) {
            require(!frozen[from], "Account frozen");
            require(!frozen[to], "Recipient frozen");
            require(allowance[from][msg.sender] >= amount, "Insufficient allowance");
            require(balanceOf[from] >= amount, "Insufficient balance");
    
            allowance[from][msg.sender] -= amount;
            balanceOf[from] -= amount;
            balanceOf[to] += amount;
            emit Transfer(from, to, uint256(amount));
            return true;
        }
    
        // --- User balance query (signed read) ---
    
        function getBalance(address account) external view returns (uint256) {
            require(msg.sender == account, "Only owner can view balance");
            return uint256(balanceOf[account]);
        }
    
        // --- Compliance functions ---
    
        function complianceBalanceOf(address account) external view returns (uint256) {
            require(
                hasRole(COMPLIANCE_ROLE, msg.sender),
                "Not authorized: requires COMPLIANCE_ROLE"
            );
            return uint256(balanceOf[account]);
        }
    
        function complianceFreeze(address account) external {
            require(
                hasRole(COMPLIANCE_ROLE, msg.sender),
                "Not authorized: requires COMPLIANCE_ROLE"
            );
            frozen[account] = true;
            emit AccountFrozen(account);
        }
    
        function complianceUnfreeze(address account) external {
            require(
                hasRole(COMPLIANCE_ROLE, msg.sender),
                "Not authorized: requires COMPLIANCE_ROLE"
            );
            frozen[account] = false;
            emit AccountUnfrozen(account);
        }
    
        // --- Auditor functions ---
    
        function auditBalanceOf(address account) external view returns (uint256) {
            require(
                hasRole(AUDITOR_ROLE, msg.sender),
                "Not authorized: requires AUDITOR_ROLE"
            );
            return uint256(balanceOf[account]);
        }
    }
    // Grant compliance role to a specific address
    token.grantRole(COMPLIANCE_ROLE, complianceOfficerAddress);
    
    // Grant auditor role
    token.grantRole(AUDITOR_ROLE, auditorAddress);
    const token = getShieldedContract({
      abi: src20Abi,
      address: SRC20_ADDRESS,
      client: adminWalletClient,
    });
    
    // Grant compliance role
    await token.write.grantRole([COMPLIANCE_ROLE, complianceOfficerAddress]);
    const complianceClient = await createShieldedWalletClient({
      chain: seismicDevnet,
      transport: http(RPC_URL),
      account: privateKeyToAccount(COMPLIANCE_OFFICER_KEY),
    });
    
    const complianceToken = getShieldedContract({
      abi: src20Abi,
      address: SRC20_ADDRESS,
      client: complianceClient,
    });
    
    // This is a signed read -- response encrypted to the compliance officer's key
    const balance = await complianceToken.read.complianceBalanceOf([targetAccount]);
    console.log("Account balance:", balance);
    npm install seismic-viem seismic-react viem wagmi @tanstack/react-query
    import { ShieldedWalletProvider } from "seismic-react";
    import { WagmiProvider, createConfig, http } from "wagmi";
    import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
    import { seismicTestnet } from "seismic-viem";
    
    const config = createConfig({
      chains: [seismicTestnet],
      transports: {
        [seismicTestnet.id]: http("https://testnet-1.seismictest.net/rpc"),
      },
    });
    
    const queryClient = new QueryClient();
    
    function App() {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <ShieldedWalletProvider config={config}>
              <TokenDashboard />
            </ShieldedWalletProvider>
          </QueryClientProvider>
        </WagmiProvider>
      );
    }
    import { useShieldedContract } from "seismic-react";
    import { src20Abi } from "./abi";
    
    const SRC20_ADDRESS = "0x1234..."; // Your deployed contract address
    
    function useToken() {
      const contract = useShieldedContract({
        address: SRC20_ADDRESS,
        abi: src20Abi,
      });
    
      return contract;
    }
    import { useSignedReadContract } from 'seismic-react';
    import { useAccount } from 'wagmi';
    import { useEffect, useState } from 'react';
    import { formatEther } from 'viem';
    
    function BalanceDisplay() {
      const { address } = useAccount();
      const [balance, setBalance] = useState<bigint | null>(null);
    
      const { signedRead, isLoading, error } = useSignedReadContract({
        address: SRC20_ADDRESS,
        abi: src20Abi,
        functionName: 'balanceOf',
        args: [address],
      });
    
      useEffect(() => {
        if (signedRead) {
          signedRead().then(setBalance);
        }
      }, [signedRead]);
    
      if (isLoading) return <p>Loading balance...</p>;
      if (error) return <p>Error loading balance</p>;
    
      return (
        <div>
          <h2>Your Balance</h2>
          <p>{formatEther(balance ?? 0n)} SRC</p>
        </div>
      );
    }
    import { useShieldedWriteContract } from 'seismic-react';
    import { parseEther } from 'viem';
    import { useState } from 'react';
    
    function TransferForm() {
      const [recipient, setRecipient] = useState('');
      const [amount, setAmount] = useState('');
    
      const { writeContract, isLoading, error, hash } = useShieldedWriteContract();
    
      const handleTransfer = () => {
        writeContract({
          address: SRC20_ADDRESS,
          abi: src20Abi,
          functionName: 'transfer',
          args: [recipient, parseEther(amount)],
        });
      };
    
      return (
        <div>
          <h2>Transfer Tokens</h2>
          <input
            type="text"
            placeholder="Recipient address (0x...)"
            value={recipient}
            onChange={(e) => setRecipient(e.target.value)}
          />
          <input
            type="text"
            placeholder="Amount"
            value={amount}
            onChange={(e) => setAmount(e.target.value)}
          />
          <button onClick={handleTransfer} disabled={isLoading}>
            {isLoading ? 'Sending...' : 'Transfer'}
          </button>
          {hash && <p>Transfer submitted: {hash}</p>}
          {error && <p>Error: {error.message}</p>}
        </div>
      );
    }
    import { useAccount } from "wagmi";
    import { BalanceDisplay } from "./BalanceDisplay";
    import { TransferForm } from "./TransferForm";
    
    function TokenDashboard() {
      const { address, isConnected } = useAccount();
    
      if (!isConnected) {
        return (
          <div>
            <h1>SRC20 Token Dashboard</h1>
            <p>Connect your wallet to get started.</p>
            {/* Add your wallet connect button here (e.g., RainbowKit, Privy, AppKit) */}
          </div>
        );
      }
    
      return (
        <div>
          <h1>SRC20 Token Dashboard</h1>
          <p>Connected: {address}</p>
          <BalanceDisplay />
          <TransferForm />
        </div>
      );
    }
        // Event to log resets.
        event Reset(uint256 indexed newRound, uint256 remainingClownStamina);
    
        // Reset the beatdown for a new round.
        function reset() public requireDown {
            clownStamina = initialClownStamina; // Reset stamina.
            secretIndex = suint256(_randomIndex()); // Pick a new random secret.
            round++; // Move to the next round.
            emit Reset(round, clownStamina); // Log the reset.
        }
        // Tracks the number of hits per player per round.
        mapping(uint256 => mapping(address => uint256)) hitsPerRound;
        // Hit the clown to reduce stamina.
        function hit() public requireStanding {
            clownStamina--; // Decrease stamina.
            hitsPerRound[round][msg.sender]++; // Record the player's hit for the current round.
            emit Hit(round, msg.sender, clownStamina); // Log the hit.
        }
        // Modifier to ensure the caller has contributed in the current round.
        modifier onlyContributor() {
            require(hitsPerRound[round][msg.sender] > 0, "NOT_A_CONTRIBUTOR");
            _;
        }
        // Reveal secret once the clown is down and the caller contributed.
        function rob() public view requireDown onlyContributor returns (bytes memory) {
            sbytes memory secret = secrets[uint256(secretIndex)];
            return bytes(secret); // Return the randomly selected secret.
        }
    // SPDX-License-Identifier: MIT License
    pragma solidity ^0.8.13;
    
    contract ClownBeatdown {
        uint256 initialClownStamina; // Starting stamina restored on reset.
        uint256 clownStamina; // Remaining stamina before the clown is down.
        uint256 round; // The current round number.
    
        mapping(uint256 => sbytes) secrets; // Pool of possible secrets (shielded).
        uint256 secretsCount; // Number of secrets for modular arithmetic.
        suint256 secretIndex; // Shielded index into the secrets mapping.
    
        // Tracks the number of hits per player per round.
        mapping(uint256 => mapping(address => uint256)) hitsPerRound;
    
        // Events to log hits and resets.
    
        // Event to log hits.
        event Hit(uint256 indexed round, address indexed hitter, uint256 remaining); // Logged when a hit lands.
        // Event to log resets.
        event Reset(uint256 indexed newRound, uint256 remainingClownStamina);
    
        constructor(uint256 _clownStamina) {
            initialClownStamina = _clownStamina; // Set starting stamina.
            clownStamina = _clownStamina; // Initialize remaining stamina.
            round = 1; // Start with the first round.
        }
    
        // Get the current clown stamina.
        function getClownStamina() public view returns (uint256) {
            return clownStamina;
        }
    
        function addSecret(string memory _secret) public {
            secrets[secretsCount] = sbytes(_secret);
            secretsCount++;
            secretIndex = suint256(_randomIndex()); // Re-pick a random secret.
        }
    
        // Hit the clown to reduce stamina.
        function hit() public requireStanding {
            clownStamina--; // Decrease stamina.
            hitsPerRound[round][msg.sender]++; // Record the player's hit for the current round.
            emit Hit(round, msg.sender, clownStamina); // Log the hit.
        }
    
    
        // Reset the beatdown for a new round.
        function reset() public requireDown {
            clownStamina = initialClownStamina; // Reset stamina.
            secretIndex = suint256(_randomIndex()); // Pick a new random secret.
            round++; // Move to the next round.
            emit Reset(round, clownStamina); // Log the reset.
        }
    
        // Reveal secret once the clown is down and the caller contributed.
     function rob() public view requireDown onlyContributor returns (bytes memory) {
            sbytes memory secret = secrets[uint256(secretIndex)];
            return bytes(secret); // Return the randomly selected secret.
        }
    
        // Generate a pseudo-random index into the secrets array.
        function _randomIndex() private view returns (uint256) {
            return uint256(keccak256(abi.encodePacked(block.prevrandao, block.timestamp, round))) % secretsCount;
        }
    
        // Modifier to ensure the clown is down.
        modifier requireDown() {
            require(clownStamina == 0, "CLOWN_STILL_STANDING");
            _;
        }
    
        // Modifier to ensure the clown is still standing.
        modifier requireStanding() {
            require(clownStamina > 0, "CLOWN_ALREADY_DOWN");
            _;
        }
    
        // Modifier to ensure the caller has contributed in the current round.
        modifier onlyContributor() {
            require(hitsPerRound[round][msg.sender] > 0, "NOT_A_CONTRIBUTOR");
            _;
        }
    }
    npm install seismic-react
    yarn add seismic-react
    pnpm add seismic-react
    bun add seismic-react
    npm install react wagmi viem seismic-viem @tanstack/react-query
    npm install @rainbow-me/rainbowkit
    import { getDefaultConfig } from "@rainbow-me/rainbowkit";
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    const config = getDefaultConfig({
      appName: "My Seismic App",
      projectId: "YOUR_WALLETCONNECT_PROJECT_ID",
      chains: [seismicTestnet],
    });
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit'
    import { ShieldedWalletProvider } from 'seismic-react'
    import { seismicTestnet } from 'seismic-react/rainbowkit'
    
    const config = getDefaultConfig({
      appName: 'My Seismic App',
      projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
      chains: [seismicTestnet],
    })
    
    const queryClient = new QueryClient()
    
    function App() {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                <YourApp />
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    // Main entry
    import {
      ShieldedWalletProvider,
      useShieldedWallet,
      useShieldedContract,
      useShieldedWriteContract,
      useSignedReadContract,
    } from "seismic-react";
    
    // Chain configs for RainbowKit
    import { seismicTestnet, sanvil } from "seismic-react/rainbowkit";
    npm install seismic-viem
    npm install wagmi@latest viem@latest
    // Correct
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    // Incorrect -- will not resolve
    import { seismicTestnet } from "seismic-react";
    {
      "resolutions": {
        "react": "^18.0.0",
        "react-dom": "^18.0.0"
      }
    }
    npm install seismic-react seismic-viem wagmi viem @rainbow-me/rainbowkit @tanstack/react-query
    // config.ts
    import { getDefaultConfig } from "@rainbow-me/rainbowkit";
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    export const config = getDefaultConfig({
      appName: "Seismic Basic dApp",
      projectId: "YOUR_WALLETCONNECT_PROJECT_ID",
      chains: [seismicTestnet],
    });
    // providers.tsx
    'use client'
    
    import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider } from 'seismic-react'
    import { config } from './config'
    import '@rainbow-me/rainbowkit/styles.css'
    
    const queryClient = new QueryClient()
    
    export function Providers({ children }: { children: React.ReactNode }) {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                {children}
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    // ShieldedCounter.tsx
    'use client'
    
    import { useShieldedWriteContract, useSignedReadContract } from 'seismic-react'
    import { useState } from 'react'
    
    const CONTRACT_ADDRESS = '0x...' as const
    const ABI = [
      {
        name: 'increment',
        type: 'function',
        stateMutability: 'nonpayable',
        inputs: [],
        outputs: [],
      },
      {
        name: 'getCount',
        type: 'function',
        stateMutability: 'view',
        inputs: [],
        outputs: [{ name: '', type: 'uint256' }],
      },
    ] as const
    
    export function ShieldedCounter() {
      const [count, setCount] = useState<string | null>(null)
    
      const { writeContract, isLoading: isWriting, hash, error: writeError } = useShieldedWriteContract({
        address: CONTRACT_ADDRESS,
        abi: ABI,
        functionName: 'increment',
      })
    
      const { signedRead, isLoading: isReading, error: readError } = useSignedReadContract({
        address: CONTRACT_ADDRESS,
        abi: ABI,
        functionName: 'getCount',
      })
    
      const handleIncrement = async () => {
        try {
          await writeContract()
        } catch (err) {
          console.error('Shielded write failed:', err)
        }
      }
    
      const handleRead = async () => {
        try {
          const result = await signedRead()
          setCount(result?.toString() ?? 'unknown')
        } catch (err) {
          console.error('Signed read failed:', err)
        }
      }
    
      return (
        <div>
          <h2>Shielded Counter</h2>
    
          <button onClick={handleIncrement} disabled={isWriting}>
            {isWriting ? 'Sending...' : 'Increment (Shielded Write)'}
          </button>
          {hash && <p>Tx hash: {hash}</p>}
          {writeError && <p>Write error: {writeError.message}</p>}
    
          <button onClick={handleRead} disabled={isReading}>
            {isReading ? 'Reading...' : 'Get Count (Signed Read)'}
          </button>
          {count !== null && <p>Count: {count}</p>}
          {readError && <p>Read error: {readError.message}</p>}
        </div>
      )
    }
    // App.tsx
    import { ConnectButton } from '@rainbow-me/rainbowkit'
    import { useShieldedWallet } from 'seismic-react'
    import { ShieldedCounter } from './ShieldedCounter'
    
    export function App() {
      const { loaded, error } = useShieldedWallet()
    
      return (
        <div>
          <h1>Seismic Basic dApp</h1>
          <ConnectButton />
    
          {error && <p>Wallet error: {error}</p>}
          {loaded ? (
            <ShieldedCounter />
          ) : (
            <p>Connect your wallet to interact with shielded contracts.</p>
          )}
        </div>
      )
    }
    User connects wallet
            |
            v
    ShieldedWalletProvider creates shielded clients (ECDH with TEE)
            |
            v
    useShieldedWriteContract    useSignedReadContract
      - encrypts calldata         - signs the read request
      - sends TxSeismic           - node verifies identity
      - returns tx hash           - returns decrypted result
    import { useSignedReadContract } from 'seismic-react'
    import { useState } from 'react'
    
    const abi = [
      {
        name: 'balanceOf',
        type: 'function',
        stateMutability: 'view',
        inputs: [],
        outputs: [{ name: '', type: 'uint256' }],
      },
    ] as const
    
    function ShieldedBalance() {
      const [balance, setBalance] = useState<string | null>(null)
    
      const { signedRead, isLoading, error } = useSignedReadContract({
        address: '0x1234567890abcdef1234567890abcdef12345678',
        abi,
        functionName: 'balanceOf',
      })
    
      async function fetchBalance() {
        const result = await signedRead()
        if (result !== undefined) {
          setBalance(result.toString())
        }
      }
    
      return (
        <div>
          <button onClick={fetchBalance} disabled={isLoading}>
            {isLoading ? 'Reading...' : 'Get Balance'}
          </button>
          {balance && <p>Balance: {balance}</p>}
          {error && <p>Error: {error.message}</p>}
        </div>
      )
    }
    import { useSignedReadContract } from 'seismic-react'
    
    function ReadWithLoadingState() {
      const { signedRead, isLoading, error } = useSignedReadContract({
        address: CONTRACT_ADDRESS,
        abi,
        functionName: 'getSecret',
      })
    
      return (
        <div>
          <button onClick={signedRead} disabled={isLoading}>
            {isLoading ? 'Fetching...' : 'Read Secret'}
          </button>
          {isLoading && <span>Please wait, signing and decrypting...</span>}
        </div>
      )
    }
    import { useSignedReadContract } from 'seismic-react'
    
    function ReadWithErrorHandling() {
      const { signedRead, error } = useSignedReadContract({
        address: CONTRACT_ADDRESS,
        abi,
        functionName: 'balanceOf',
      })
    
      async function handleRead() {
        try {
          const result = await signedRead()
          console.log('Result:', result)
        } catch (e) {
          console.error('Signed read failed:', e)
        }
      }
    
      return (
        <div>
          <button onClick={handleRead}>Read</button>
          {error && <p style={{ color: 'red' }}>Last error: {error.message}</p>}
        </div>
      )
    }
    import { getDefaultConfig } from "@rainbow-me/rainbowkit";
    import { seismicTestnet } from "seismic-react/rainbowkit";
    
    const config = getDefaultConfig({
      appName: "My Seismic App",
      projectId: "YOUR_WALLETCONNECT_PROJECT_ID",
      chains: [seismicTestnet],
    });
    import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider } from 'seismic-react'
    import '@rainbow-me/rainbowkit/styles.css'
    
    const queryClient = new QueryClient()
    
    function App({ children }: { children: React.ReactNode }) {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                {children}
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    import { ConnectButton } from '@rainbow-me/rainbowkit'
    
    function Header() {
      return (
        <header>
          <ConnectButton />
        </header>
      )
    }
    import { useShieldedWallet } from 'seismic-react'
    
    function MyComponent() {
      const { walletClient, loaded, error } = useShieldedWallet()
    
      if (!loaded) return <div>Initializing shielded wallet...</div>
      if (error) return <div>Error: {error}</div>
    
      return <div>Connected!</div>
    }
    // app/providers.tsx
    'use client'
    
    import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit'
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider } from 'seismic-react'
    import { seismicTestnet } from 'seismic-react/rainbowkit'
    import '@rainbow-me/rainbowkit/styles.css'
    
    const config = getDefaultConfig({
      appName: 'My Seismic App',
      projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
      chains: [seismicTestnet],
    })
    
    const queryClient = new QueryClient()
    
    export function Providers({ children }: { children: React.ReactNode }) {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                {children}
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    // app/layout.tsx
    import { Providers } from './providers'
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html lang="en">
          <body>
            <Providers>{children}</Providers>
          </body>
        </html>
      )
    }
    import { getDefaultConfig } from "@rainbow-me/rainbowkit";
    import { seismicTestnet, sanvil } from "seismic-react/rainbowkit";
    
    const config = getDefaultConfig({
      appName: "My Seismic App",
      projectId: "YOUR_WALLETCONNECT_PROJECT_ID",
      chains: [
        ...(process.env.NODE_ENV === "development" ? [sanvil] : []),
        seismicTestnet,
      ],
    });
    'use client'
    
    import { RainbowKitProvider, ConnectButton, getDefaultConfig } from '@rainbow-me/rainbowkit'
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { ShieldedWalletProvider, useShieldedWallet } from 'seismic-react'
    import { seismicTestnet } from 'seismic-react/rainbowkit'
    import '@rainbow-me/rainbowkit/styles.css'
    
    const config = getDefaultConfig({
      appName: 'My Seismic App',
      projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
      chains: [seismicTestnet],
    })
    
    const queryClient = new QueryClient()
    
    function WalletStatus() {
      const { walletClient, publicClient, loaded, error } = useShieldedWallet()
    
      if (!loaded) return <p>Initializing shielded wallet...</p>
      if (error) return <p>Error: {error}</p>
      if (!walletClient) return <p>Connect your wallet to get started.</p>
    
      return (
        <div>
          <p>Shielded wallet ready</p>
          <p>Public client: {publicClient ? 'Available' : 'Loading...'}</p>
        </div>
      )
    }
    
    export default function App() {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                <ConnectButton />
                <WalletStatus />
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    pip install seismic-web3
    uv add seismic-web3
    import os
    from seismic_web3 import SEISMIC_TESTNET, PrivateKey
    
    pk = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    w3 = SEISMIC_TESTNET.wallet_client(pk)
    import os
    from seismic_web3 import SEISMIC_TESTNET, PrivateKey
    
    pk = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
    # HTTP
    w3 = await SEISMIC_TESTNET.async_wallet_client(pk)
    
    # WebSocket (auto-selects ws_url from chain config)
    w3 = await SEISMIC_TESTNET.async_wallet_client(pk, ws=True)
    from seismic_web3 import SEISMIC_TESTNET
    
    # Sync
    public = SEISMIC_TESTNET.public_client()
    
    # Async
    public = SEISMIC_TESTNET.async_public_client()
    import os
    from seismic_web3 import SEISMIC_TESTNET, SANVIL, PrivateKey
    
    # Seismic testnet
    pk = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    w3 = SEISMIC_TESTNET.wallet_client(pk)
    
    # Sanvil testnet
    w3 = SANVIL.wallet_client(pk)
    import { useShieldedContract } from 'seismic-react'
    
    const abi = [
      {
        name: 'balanceOf',
        type: 'function',
        stateMutability: 'view',
        inputs: [],
        outputs: [{ name: '', type: 'uint256' }],
      },
      {
        name: 'transfer',
        type: 'function',
        stateMutability: 'nonpayable',
        inputs: [
          { name: 'to', type: 'address' },
          { name: 'amount', type: 'uint256' },
        ],
        outputs: [],
      },
    ] as const
    
    function MyContract() {
      const { contract, error } = useShieldedContract({
        abi,
        address: '0x1234567890abcdef1234567890abcdef12345678',
      })
    
      if (error) return <div>Error: {error.message}</div>
      if (!contract) return <div>Loading contract...</div>
    
      return <div>Contract ready</div>
    }
    import { useShieldedContract } from 'seismic-react'
    import { useState } from 'react'
    
    function TokenActions() {
      const [balance, setBalance] = useState<bigint | null>(null)
      const { contract } = useShieldedContract({ abi, address: CONTRACT_ADDRESS })
    
      async function readBalance() {
        if (!contract) return
        const result = await contract.read.balanceOf()
        setBalance(result as bigint)
      }
    
      async function transfer() {
        if (!contract) return
        const hash = await contract.write.transfer(['0xRecipient...', 100n])
        console.log('Transfer tx:', hash)
      }
    
      return (
        <div>
          <button onClick={readBalance}>Check Balance</button>
          {balance !== null && <p>Balance: {balance.toString()}</p>}
          <button onClick={transfer}>Transfer</button>
        </div>
      )
    }
    const abi = [
      {
        name: "increment",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [],
        outputs: [],
      },
      {
        name: "number",
        type: "function",
        stateMutability: "view",
        inputs: [],
        outputs: [{ name: "", type: "uint256" }],
      },
    ] as const;
    
    // TypeScript now infers the available methods and their argument/return types
    const { contract } = useShieldedContract({ abi, address: "0x..." });
    cd packages
    bun create vite web --template react-ts
    cd web
    bun add [email protected] [email protected] viem@^2.22.3 \
      wagmi@^2.0.0 @rainbow-me/rainbowkit@^2.0.0 \
      @tanstack/react-query@^5.55.3 \
      @mui/material@^6.4.3 @emotion/react @emotion/styled \
      framer-motion@^12.7.3 react-router-dom@^7.1.4 \
      react-toastify@^11.0.5 use-sound@^5.0.0 \
      react-redux@^9.2.0 @reduxjs/toolkit@^2.5.1 \
      @tailwindcss/vite tailwindcss@^4
    bun add -d @vitejs/plugin-react-swc
    import { resolve } from "path";
    import { defineConfig } from "vite";
    
    import tailwindcss from "@tailwindcss/vite";
    import react from "@vitejs/plugin-react-swc";
    
    // https://vite.dev/config/
    export default defineConfig({
      plugins: [react(), tailwindcss()],
      envDir: resolve(__dirname, "../.."),
      resolve: {
        alias: {
          "@": resolve(__dirname, "src"),
        },
      },
    });
    VITE_CHAIN_ID=31337
    VITE_RPC_URL=http://127.0.0.1:8545
    VITE_FAUCET_URL=https://faucet-2.seismicdev.net/
    import React from 'react'
    import { PropsWithChildren, useCallback } from 'react'
    import { BrowserRouter, Route, Routes } from 'react-router-dom'
    import {
      type OnAddressChangeParams,
      ShieldedWalletProvider,
    } from 'seismic-react'
    import { sanvil, seismicTestnet } from 'seismic-react/rainbowkit'
    import { http } from 'viem'
    import { type Config, WagmiProvider } from 'wagmi'
    
    import { AuthProvider } from '@/components/chain/WalletConnectButton'
    import Home from '@/pages/Home'
    import NotFound from '@/pages/NotFound'
    import { getDefaultConfig } from '@rainbow-me/rainbowkit'
    import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    
    import './App.css'
    
    const configuredChainId = String(import.meta.env.VITE_CHAIN_ID ?? '')
    const isSanvilConfig =
      configuredChainId === 'sanvil' || configuredChainId === String(sanvil.id)
    const CHAIN = isSanvilConfig ? sanvil : seismicTestnet
    const CHAINS = [CHAIN]
    
    const config = getDefaultConfig({
      appName: 'Seismic Starter',
      projectId: 'd705c8eaf9e6f732e1ddb8350222cdac',
      // @ts-expect-error: this is fine
      chains: CHAINS,
      ssr: false,
    })
    
    const client = new QueryClient()
    
    const Providers: React.FC<PropsWithChildren<{ config: Config }>> = ({
      config,
      children,
    }) => {
      const publicChain = CHAINS[0]
      const publicTransport = http(publicChain.rpcUrls.default.http[0])
      const handleAddressChange = useCallback(
        async ({ publicClient, address }: OnAddressChangeParams) => {
          if (publicClient.chain.id !== sanvil.id) return
    
          const existingBalance = await publicClient.getBalance({ address })
          if (existingBalance > 0n) return
    
          const setBalance = publicClient.request as unknown as (args: {
            method: string
            params?: unknown[]
          }) => Promise<unknown>
    
          await setBalance({
            method: 'anvil_setBalance',
            params: [address, `0x${(10_000n * 10n ** 18n).toString(16)}`],
          })
        },
        []
      )
    
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={client}>
            <RainbowKitProvider>
              <ShieldedWalletProvider
                config={config}
                options={{
                  publicTransport,
                  publicChain,
                  onAddressChange: handleAddressChange,
                }}
              >
                <AuthProvider>{children}</AuthProvider>
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    
    const App: React.FC = () => {
      return (
        <BrowserRouter>
          <Providers config={config}>
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="*" element={<NotFound />} />
            </Routes>
          </Providers>
        </BrowserRouter>
      )
    }
    
    export default App
    import { configureStore } from '@reduxjs/toolkit'
    
    export const store = configureStore({
      reducer: {},
    })
    import { createTheme } from '@mui/material/styles'
    
    const theme = createTheme({
      palette: {
        mode: 'dark',
      },
    })
    
    export default theme
    import ClownPuncher from '@/components/game/ClownPuncher'
    
    const Home = () => <ClownPuncher />
    export default Home
    const NotFound = () => <div>404 - Page not found</div>
    export default NotFound
    @import "tailwindcss";
    import { StrictMode } from 'react'
    import { createRoot } from 'react-dom/client'
    import { Provider } from 'react-redux'
    import { ToastContainer } from 'react-toastify'
    import 'react-toastify/dist/ReactToastify.css'
    
    import App from '@/App.tsx'
    import { store } from '@/store/store'
    import theme from '@/theme.ts'
    import { ThemeProvider } from '@mui/material/styles'
    
    import './index.css'
    
    createRoot(document.getElementById('root')!).render(
      <StrictMode>
        <ThemeProvider theme={theme}>
          <Provider store={store}>
            <App />
            <ToastContainer />
          </Provider>
        </ThemeProvider>
      </StrictMode>
    )
    // BAD: Leaks the value of `isVIP` via gas difference
    sbool isVIP = /* ... */;
    if (isVIP) {
        discount = 50s;
    } else {
        // extra work only in the else branch — gas difference reveals which path ran
        suint256 tmp = 0s;
        for (uint256 i = 0; i < 10; i++) {
            tmp = tmp + 1s;
        }
        discount = tmp;
    }
    // BETTER: Both arms are identical operations (single assignment), same gas either way
    discount = isVIP ? 50s : 0s;
    // Both forms embed `42` in bytecode
    suint256 a = suint256(42);
    suint256 b = 42s;
    // BAD: Leaks the value of `shieldedCount` via gas
    suint256 shieldedCount = /* ... */;
    for (uint256 i = 0; i < uint256(shieldedCount); i++) {
        // Each iteration is visible in gas cost
    }
    // BETTER: Fixed-size loop with constant iteration count
    uint256 constant MAX_ITERATIONS = 100;
    for (uint256 i = 0; i < MAX_ITERATIONS; i++) {
        // Use a shielded condition to decide whether to actually do work.
        // IMPORTANT: Both the "real work" and "no-op" paths must use
        // the same gas -- e.g., write to the same slots, do the same
        // number of arithmetic ops, etc.
    }
    // BAD: Anyone can call this and read the shielded value
    suint256 private _secretBalance;
    
    function secretBalance() external view returns (uint256) {
        return uint256(_secretBalance);
    }
    suint256 private _secretBalance;
    
    function secretBalance() external view returns (uint256) {
        require(msg.sender == owner, "Not authorized");
        return uint256(_secretBalance);
    }
    // Will NOT compile
    suint256 public secretBalance;
    
    // Will NOT compile
    function getSecret() external view returns (suint256) {
        return secretBalance;
    }
    // This function accepts shielded input, but nothing prevents calling it
    // with a regular (non-Seismic) transaction — which would leak `amount`.
    function deposit(suint256 amount) external {
        balances[msg.sender] += amount;
    }
    // BAD: Gas cost reveals the value of `shieldedExp`
    suint256 base = 2s;
    suint256 shieldedExp = /* ... */;
    suint256 result = base ** shieldedExp;  // Gas cost leaks shieldedExp
    enum Status { Active, Inactive, Suspended }
    // Only 3 possible values (0, 1, 2) — shielding provides little protection
    suint256 shieldedStatus = suint256(uint256(Status.Active));
    // Will NOT compile — compiler error
    suint256 immutable SECRET = 42s;
    
    // Will NOT compile — compiler error
    suint256 constant MY_VALUE = 1s;
    // In theory, a proposer could simulate this output and decide
    // whether to include the transaction based on the result.
    function drawWinner() external {
        suint256 rand = rng256();
        uint256 winnerIndex = uint256(rand) % participants.length;
        winner = participants[winnerIndex];
    }
    // An attacker wraps the call and reverts on unfavorable outcomes,
    // making every attempt risk-free.
    function playLotteryRiskFree() external {
        bool winner = lotteryContract.playLottery();
        require(winner, "Better luck next time");
    }
    suint256 x = 42s;            // inferred as suint256
    suint8 small = 7s;           // inferred as suint8
    sint256 neg = -1s;           // inferred as sint256
    suint256 x = suint256(42);
    suint8 small = suint8(7);
    sint256 neg = sint256(-1);
    suint256 a = 1_000s;         // underscores
    suint256 b = 0xDEADs;        // hex
    suint256 c = 1e5s;           // scientific notation
    sint256  d = -42s;           // unary minus
    suint8  a = 255s;            // suint8
    suint32 b = 1_000s;          // suint32
    sint128 c = -1s;             // sint128
    
    suint256 x = 10s;
    suint256 y = x + 5s;         // 5s inferred as suint256 from context
    suint256 x = 1s + 1;         // Error — mixed shielded/non-shielded
    suint256 x = 1s + 1s;        // OK
    uint256 x = 42s;             // Error — cannot assign shielded to uint256
    0x66
    .
  • Emit a regular event containing the encrypted bytes. Since the event parameter is bytes (not a shielded type), this compiles and works normally.

  • Recipient decrypts the data using the AES-GCM Decrypt precompile at address 0x67, either off-chain or on-chain if needed.

  • Decrypt data with AES-GCM

    HKDF

    Key derivation from shared secret

    ECDH

    0x65

    Shared secret generation

    AES-GCM Encrypt

    0x66

    Encrypt data with AES-GCM

    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    contract PrivateToken {
        address public owner;
        mapping(address => suint256) private balances;
        mapping(address => bytes) public publicKeys;
    
        sbytes32 private contractPrivateKey;
        bytes public contractPublicKey;
    
        event Transfer(address indexed from, address indexed to, bytes encryptedAmount);
    
        constructor() {
            owner = msg.sender;
        }
    
        // Owner sets the contract keypair via a Seismic transaction (type 0x4A)
        // so the private key is encrypted in calldata and never exposed.
        function setContractKey(sbytes32 _privateKey, bytes memory _publicKey) external {
            require(msg.sender == owner, "Only owner");
            require(bytes32(contractPrivateKey) == bytes32(0), "Already set");
            contractPrivateKey = _privateKey;
            contractPublicKey = _publicKey;
        }
    
        function registerPublicKey(bytes memory pubKey) external {
            publicKeys[msg.sender] = pubKey;
        }
    
        function transfer(address to, suint256 amount) public {
            require(bytes32(contractPrivateKey) != bytes32(0), "Contract key not set");
            balances[msg.sender] -= amount;
            balances[to] += amount;
    
            bytes memory recipientPubKey = publicKeys[to];
            if (recipientPubKey.length > 0) {
                // 1. Shared secret via ECDH
                bytes32 sharedSecret = ecdh(contractPrivateKey, recipientPubKey);
    
                // 2. Derive encryption key via HKDF
                sbytes32 encKey = sbytes32(hkdf(abi.encodePacked(sharedSecret)));
    
                // 3. Encrypt the amount via AES-GCM
                uint96 nonce = uint96(bytes12(keccak256(abi.encodePacked(msg.sender, to, block.number))));
                bytes memory encrypted = aes_gcm_encrypt(encKey, nonce, abi.encode(uint256(amount)));
    
                // 4. Emit with encrypted bytes
                emit Transfer(msg.sender, to, encrypted);
            }
        }
    }
    // BAD: This exposes the confidential value to everyone
    event Transfer(address from, address to, uint256 amount);
    
    function transfer(address to, suint256 amount) public {
        // ...
        emit Transfer(msg.sender, to, uint256(amount)); // Leaks the amount!
    }
    ECDH precompile
    HKDF precompile
    AES-GCM Encrypt precompile
    SRC20: Private Token
    Precompiles reference
    Python
    TypeScript (viem)

    AES-GCM Decrypt

    Send encrypted write transactions

    Perform signed, encrypted read calls

    WalletConnect AppKit integration

    Get a contract instance -> useShieldedContract
  • Install the package -> Installation

  • Set up RainbowKit -> RainbowKit Guide

  • ,
    createSeismicDevnet
    Contract Abstraction
    -- ABI-bound contract instances via
    useShieldedContract
  • wagmi/RainbowKit Integration -- Drop-in provider that composes with the standard wagmi stack

  • TypeScript Support -- Full type inference from contract ABIs

  • -- Read and write shielded contract state
    wagmi config
      └─ ShieldedWalletProvider
           ├─ ShieldedPublicClient  (encrypted reads)
           ├─ ShieldedWalletClient  (encrypted writes)
           └─ Hooks
                ├─ useShieldedWallet          Access wallet/public clients
                ├─ useShieldedContract         Contract instance with ABI binding
                ├─ useShieldedWriteContract    Encrypted write transactions
                └─ useSignedReadContract       Signed, encrypted reads

    Installation

    Package setup, peer dependencies, and configuration

    ShieldedWalletProvider

    React context provider for shielded clients

    Hooks Overview

    Summary of all available hooks

    useShieldedWallet

    Access shielded wallet and public clients

    useShieldedContract

    Create a contract instance with ABI binding

    Wallet Guides Overview

    Connecting different wallet providers

    RainbowKit

    Setup with RainbowKit wallet UI

    Privy

    Embedded wallets with Privy

    ShieldedWalletProvider
    useSignedReadContract
    useShieldedWriteContract
    ShieldedWalletProvider
    useShieldedWallet
    useShieldedContract
    useShieldedWriteContract
    useSignedReadContract
    Install seismic-react
    Set up ShieldedWalletProvider
    Connect a wallet
    Use hooks

    EIP-712 typed data signing functions

    Built-in contract ABIs and helpers

    Use async client → Client Documentation
  • Configure custom chain → Chains Documentation

  • Transaction types → UnsignedSeismicTx, SeismicElements
    ⚡ Async/Await - Full async support with AsyncWeb3
  • 🔑 EIP-712 - Structured typed data signing

  • 🛠️ Precompiles - Access Mercury EVM cryptographic precompiles

  • 🌐 Web3.py Compatible - Standard Web3 instance with Seismic extensions

  • Client

    Create sync/async wallet and public clients

    Chains

    Chain configuration (SEISMIC_TESTNET, SANVIL)

    Contract

    Interact with shielded and public contracts

    Guides

    Step-by-step tutorials and runnable examples

    API Reference

    Complete API documentation for all types and functions

    Types

    Primitive types (Bytes32, PrivateKey, etc.)

    Transaction Types

    Seismic transaction dataclasses

    Namespaces

    w3.seismic namespace methods

    Precompiles

    Privacy-preserving cryptographic functions

    SRC20

    SRC20 token standard support

    Web3 (standard web3.py)
    ├── eth (standard)
    ├── net (standard)
    └── seismic (Seismic-specific) ✨
        ├── send_shielded_transaction()
        ├── signed_call()
        ├── get_tee_public_key()
        ├── deposit()
        └── contract() → ShieldedContract
                         ├── .write (shielded)
                         ├── .read (signed)
                         ├── .twrite (transparent)
                         ├── .tread (transparent)
                         └── .dwrite (debug)
    Shielded Write Guide
    Signed Reads Guide
    SRC20 Documentation
    create_wallet_client
    create_public_client
    ShieldedContract
    PublicContract
    EncryptionState
    Precompiles
    Install and setup a client
    Send your first shielded transaction
    Explore the API reference

    hashtag
    Config
    Parameter
    Type
    Required
    Description

    address

    Hex

    Yes

    Contract address

    abi

    Abi

    hashtag
    Return Type

    Property
    Type
    Description

    writeContract

    () => Promise<Hex>

    Function to execute the shielded write

    write

    () => Promise<Hex>

    Alias for writeContract


    hashtag
    Usage

    hashtag
    Shielded token transfer

    hashtag
    Transaction hash tracking

    The hash field updates after each successful write. Use it to link to a block explorer or track confirmation.

    hashtag
    Loading and error state handling

    hashtag
    With gas override

    Pass gas or gasPrice to override the automatic estimates:

    circle-info

    Like useSignedReadContract, the write function is imperative -- you call writeContract() or write() explicitly. The hook does not auto-execute on mount or when arguments change.

    hashtag
    See Also

    • useSignedReadContract -- Perform signed, encrypted read calls

    • useShieldedContract -- Contract instance with both read and write methods

    • useShieldedWallet -- Access the underlying wallet client

    • -- Context provider required by this hook

    • -- Summary of all hooks

    import { useShieldedWriteContract } from "seismic-react";
    import { useShieldedWriteContract } from 'seismic-react'
    
    const abi = [
      {
        name: 'transfer',
        type: 'function',
        stateMutability: 'nonpayable',
        inputs: [
          { name: 'to', type: 'address' },
          { name: 'amount', type: 'uint256' },
        ],
        outputs: [],
      },
    ] as const
    
    function TransferToken() {
      const { writeContract, isLoading, error, hash } = useShieldedWriteContract({
        address: '0x1234567890abcdef1234567890abcdef12345678',
        abi,
        functionName: 'transfer',
        args: ['0xRecipientAddress...', 1000n],
      })
    
      return (
        <div>
          <button onClick={writeContract} disabled={isLoading}>
            {isLoading ? 'Sending...' : 'Transfer'}
          </button>
          {hash && <p>Transaction: {hash}</p>}
          {error && <p>Error: {error.message}</p>}
        </div>
      )
    }
    import { useShieldedWriteContract } from 'seismic-react'
    import { useEffect } from 'react'
    
    function WriteWithTracking() {
      const { writeContract, hash, isLoading } = useShieldedWriteContract({
        address: CONTRACT_ADDRESS,
        abi,
        functionName: 'increment',
      })
    
      useEffect(() => {
        if (hash) {
          console.log('Transaction confirmed:', hash)
        }
      }, [hash])
    
      return (
        <div>
          <button onClick={writeContract} disabled={isLoading}>
            Increment
          </button>
          {hash && (
            <a href={`https://seismic-testnet.socialscan.io/tx/${hash}`} target="_blank" rel="noreferrer">
              View on explorer
            </a>
          )}
        </div>
      )
    }
    import { useShieldedWriteContract } from 'seismic-react'
    
    function WriteWithStates() {
      const { writeContract, isLoading, error, hash } = useShieldedWriteContract({
        address: CONTRACT_ADDRESS,
        abi,
        functionName: 'setNumber',
        args: [42n],
      })
    
      return (
        <div>
          <button onClick={writeContract} disabled={isLoading}>
            {isLoading ? 'Encrypting & sending...' : 'Set Number'}
          </button>
          {isLoading && <p>Transaction in progress...</p>}
          {error && <p style={{ color: 'red' }}>Failed: {error.message}</p>}
          {hash && <p style={{ color: 'green' }}>Success: {hash}</p>}
        </div>
      )
    }
    import { useShieldedWriteContract } from 'seismic-react'
    
    function WriteWithGasOverride() {
      const { writeContract, isLoading } = useShieldedWriteContract({
        address: CONTRACT_ADDRESS,
        abi,
        functionName: 'expensiveOperation',
        gas: 500_000n,
        gasPrice: 20_000_000_000n,
      })
    
      return (
        <button onClick={writeContract} disabled={isLoading}>
          Execute
        </button>
      )
    }

    How Seismic Works

    Seismic adds on-chain privacy to the EVM through three layers: a modified Solidity compiler, a network of TEE-secured nodes, and a shielded storage model. This page explains how they fit together.

    hashtag
    Three pillars

    Layer
    What it does

    hashtag
    The Seismic Solidity compiler

    The Seismic compiler (ssolc) is a fork of solc that understands shielded types. When you write:

    the compiler emits CSTORE (opcode 0xB1) instead of SSTORE to write the value, and CLOAD (opcode 0xB0) instead of SLOAD to read it. These opcodes tell the EVM to treat the storage slot as private.

    The supported shielded types are:

    • suint / sint -- shielded integers (all standard bit widths: suint8 through suint256)

    • sbool -- shielded boolean

    Arithmetic, comparisons, and assignments work exactly as they do with regular Solidity types. The compiler handles routing to the correct opcodes. You do not need to call any special APIs or change your contract logic.

    hashtag
    The Seismic network

    Seismic is an EVM-compatible L1 blockchain. Nodes run inside Trusted Execution Environments (TEEs) powered by Intel TDX. The TEE creates a hardware-enforced enclave: code and data inside the enclave cannot be observed or tampered with by the host operating system or the node operator.

    Key network properties:

    • Block time: Sub-second, powered by (our custom consensus algorithm)

    • Finality: 1 block

    • Transaction types: All standard Ethereum types (Legacy, EIP-1559, EIP-2930, EIP-4844, EIP-7702) plus the Seismic transaction type 0x4A

    hashtag
    The Seismic transaction (type 0x4A)

    The diagram below shows the block structure, state tries, and the Seismic transaction format (TxSeismic). Note how each storage slot in the Account Storage trie carries a boolean flag -- (u256, true) for private slots and (u256, false) for public slots.

    Standard Ethereum transactions send calldata in plaintext. The Seismic transaction type encrypts calldata before it leaves the user's machine.

    The encryption flow:

    1. The client calls seismic_getTeePublicKey on the RPC to fetch the network's TEE public key.

    2. The client performs ECDH key agreement between the user's private key and the TEE public key to derive a shared secret.

    3. The calldata is encrypted using AEAD (authenticated encryption with associated data).

    At no point is the plaintext calldata visible outside the TEE -- not in the mempool, not in block data, not in transaction traces. For a deeper dive, see .

    hashtag
    Shielded storage (FlaggedStorage)

    The diagram below shows how the RPC layer, EVM, and storage interact. Notice that eth_getStorageAt is blocked for shielded slots, and cross-contract reads of private storage return zero.

    Seismic extends the EVM storage model with FlaggedStorage. Each storage slot is a tuple:

    When a contract uses CSTORE to write a value, the is_private flag is set to true. This flag has two effects:

    • eth_getStorageAt returns zero for shielded slots, making them indistinguishable from uninitialized storage. External observers cannot read or detect shielded data through the standard RPC.

    • Only CLOAD can read private slots. The standard SLOAD opcode cannot access them. This is enforced in the .

    The compiler manages this automatically. When you declare a variable as suint256, the compiler emits CLOAD/CSTORE. When you declare it as uint256, the compiler emits SLOAD/SSTORE. You do not interact with FlaggedStorage directly.

    hashtag
    End-to-end walkthrough

    Here is what happens when a user calls transfer() on an SRC20 contract with shielded balances:

    Step 1: User encrypts calldata. The client library (e.g., seismic-viem) fetches the TEE public key, derives a shared secret via ECDH, and encrypts the function and its arguments (to and amount) using AEAD. The encrypted payload is wrapped in a type 0x4A transaction.

    Step 2: Transaction enters the mempool. The calldata is encrypted. Observers can see that a transaction was submitted to the SRC20 contract, but the recipient address and amount are not readable.

    Step 3: Node decrypts inside the TEE. The Seismic node, running inside Intel TDX, decrypts the calldata using the network's private key. The plaintext arguments are now available only within the enclave.

    Step 4: EVM executes with CSTORE. The EVM processes the transfer() function. Reads from balanceOf use CLOAD. Writes to balanceOf use CSTORE. These storage slots will have is_private = true.

    Step 5: Storage is updated. The new balances are written to shielded storage.

    Step 6: Observers see 0x00...0. Anyone querying the contract state, reading transaction traces, or inspecting block data sees zero in place of all shielded values -- the balances, the transfer amount, and any intermediate computation involving shielded types.

    hashtag
    Precompiles

    Seismic adds six precompiled contracts to the EVM, giving smart contracts access to cryptographic primitives that would be prohibitively expensive to implement in Solidity:

    Address
    Name
    Purpose

    These precompiles enable contracts to perform on-chain encryption, key derivation, and random number generation without relying on external oracles or off-chain computation.

    hashtag
    System architecture

    The diagram below shows the full Seismic node architecture inside the TEE boundary. Encrypted transactions flow from the client through the RPC layer, into the transaction pool, through consensus, and into the block executor where calldata is decrypted and executed. Shielded results are written to storage.

    The system is composed of three components that work together to provide confidential smart contract execution:

    Component
    Role

    All three components are designed so that private data is only ever accessible inside the Trusted Execution Environment. No plaintext shielded data leaves the TEE boundary at any point in the pipeline.

    hashtag
    Seismic node

    The Seismic node is a fork of (the Rust Ethereum execution client). It handles:

    • RPC: Accepts incoming transactions and read requests. Serves responses to clients, redacting shielded data from public queries.

    • EVM execution: Runs a modified EVM that supports CLOAD/CSTORE opcodes and the six Seismic . This is built on a forked version of revm.

    The entire node process runs inside an Intel TDX Trusted Execution Environment. This means the node operator cannot inspect memory, attach debuggers, or extract keys from the running process.

    hashtag
    Fork chain

    The Seismic execution stack is built on a chain of forks from the Ethereum Rust ecosystem:

    • (fork of alloy-core): Shielded types and FlaggedStorage primitives.

    • (fork of alloy-trie): Merkle trie encoding for FlaggedStorage values.

    • (fork of revm): CLOAD/CSTORE opcodes, Seismic precompiles, and FlaggedStorage access rules.

    Plus two original repos:

    • : Consensus client.

    • : ECDH + AES-GCM crypto for transaction encryption/decryption.

    • : Rust SDK with TxSeismic type and encryption-aware providers.

    For the full list and dependency flow, see .

    hashtag
    Summit (consensus)

    Summit is Seismic's consensus layer, built on primitives. It handles:

    • Block production: Ordering transactions into blocks with sub-second block times.

    • Consensus: Reaching agreement among validators on the canonical chain.

    • Finality: Single-block finality -- once a block is produced and agreed upon, it is final.

    Summit communicates with the Seismic node to receive transactions from the mempool and to deliver finalized blocks for execution.

    hashtag
    Enclave

    The Enclave component manages all TEE-related operations. It is the trust anchor of the system.

    Key management:

    • Genesis node: When the network starts, the genesis node generates a root key inside the TEE. This key never leaves the enclave.

    • Peer nodes: When a new node joins the network, it must pass remote attestation before receiving the root key. The existing nodes verify that the new node is running identical, approved code inside a genuine TEE.

    • Encryption secret key: The root key is used to derive the network's encryption secret key. This key is used to decrypt the calldata of Seismic transactions (type 0x4A).

    Attestation:

    Remote attestation is the process by which one TEE proves to another that it is running approved code on genuine hardware. In Seismic:

    1. A new node generates an attestation report inside its TEE.

    2. The report is sent to existing nodes for verification.

    3. Existing nodes check that the report was generated by genuine Intel TDX hardware and that the code measurement matches the approved build.

    This ensures that every node in the network is running the same software and that no node can be modified to leak private data.

    hashtag
    TEE guarantees

    The TEE (Trusted Execution Environment) is the foundation of Seismic's privacy model. Intel TDX provides the following guarantees:

    hashtag
    Code integrity

    Remote attestation ensures that all nodes in the network are running identical, approved code. A node cannot be modified to log private data, skip encryption, or export keys.

    hashtag
    Memory isolation

    The TEE creates a hardware-enforced boundary around the node's memory. The host operating system, hypervisor, and node operator cannot read or write to the enclave's memory space.

    hashtag
    Key protection

    Cryptographic keys (the root key, encryption secret key, and any derived keys) are generated inside the TEE and never leave it. There is no API to export keys from the enclave. Even if the node operator has full root access to the host machine, they cannot extract the keys.

    hashtag
    What TEEs do not protect against

    • Side-channel attacks: While Intel TDX mitigates many known side-channel attacks, this is an active area of research. Seismic's design minimizes the attack surface, but hardware-level side channels remain a theoretical concern.

    • Bugs in the node software: If the approved node code has a bug that leaks private data through a public channel (e.g., writing shielded values to public storage), the TEE will faithfully execute that buggy code. This is why the code is open-source and subject to audit.

    • Transaction metadata: The TEE protects calldata and storage values, but metadata such as sender address, gas usage, and the target contract address remain visible on-chain.

    seismic-viem

    TypeScript client library for Seismic, composing with viem to add shielded transactions, encrypted calldata, and signed reads.

    Low-level TypeScript SDK for Seismicarrow-up-right, built on viemarrow-up-right 2.x. Provides shielded public and wallet clients, encrypted contract interactions, signed reads, chain configs, and precompile access. This is the foundation that seismic-react builds upon.

    npm install seismic-viem viem

    hashtag
    Quick Example

    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { createShieldedWalletClient, seismicTestnet } from "seismic-viem";
    
    const client = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    
    // Smart write — auto-detects shielded params, encrypts only when needed
    const hash = await client.writeContract({
      address: "0x...",
      abi: myContractAbi,
      functionName: "transfer", // has suint256/saddress params → encrypted automatically
      args: ["0x...", 100n],
    });
    
    // Smart read — auto-detects shielded params, uses signed read only when needed
    const balance = await client.readContract({
      address: "0x...",
      abi: myContractAbi,
      functionName: "balanceOf", // has saddress param → signed read automatically
      args: ["0x..."],
    });

    hashtag
    Architecture

    hashtag
    Documentation Navigation

    hashtag
    Getting Started

    Section
    Description

    hashtag
    Client Reference

    Section
    Description

    hashtag
    Contract Interaction

    Section
    Description

    hashtag
    Infrastructure

    Section
    Description

    hashtag
    Features

    • Shielded Transactions -- Encrypt calldata with TEE public key via AES-GCM before sending

    • Signed Reads -- Prove identity in eth_call with signed read requests

    • Two Client Types -- createShieldedPublicClient (read-only) and createShieldedWalletClient

    hashtag
    Quick Links

    hashtag
    By Task

    • Create a wallet client -> Shielded Wallet Client

    • Create a read-only client -> Shielded Public Client

    • Interact with a contract -> Contract Instance

    hashtag
    By Component

    • Client types -> Shielded Public Client, Shielded Wallet Client

    • Contract interaction -> Contract Instance, Shielded Writes, Signed Reads

    • Chains -> (seismicTestnet, sanvil, createSeismicDevnet

    hashtag
    Comparison with seismic-react

    Aspect
    seismic-viem
    seismic-react
    circle-info

    If you are building a React application, consider using which provides React hooks on top of seismic-viem. For Node.js scripts, server-side code, or non-React frontends, use seismic-viem directly.

    hashtag
    Next Steps

    1. -- Add the package to your project

    2. -- Connect to testnet or local dev

    3. Create a shielded wallet client -- Connect with full capabilities

    Namespaces

    Contract interaction namespaces for shielded and transparent operations

    Seismic contracts expose five namespaces for different types of operations. Each namespace provides a different combination of encryption, authentication, and transaction behavior.


    hashtag
    Overview

    When you instantiate a contract:

    contract = w3.seismic.contract(address="0x...", abi=ABI)

    You get access to five namespaces:


    hashtag
    Quick Comparison

    hashtag
    Encrypted vs Transparent

    Encrypted (.write, .read, .dwrite):

    • Calldata is encrypted using AES-GCM

    • Only you and the TEE can see plaintext

    • Requires ECDH key exchange with node

    • Includes security metadata (nonce, block hash, expiry)

    Transparent (.twrite, .tread):

    • Calldata is visible on-chain

    • Standard Ethereum transactions/calls

    • No encryption or security metadata

    • Lower gas cost (no overhead)

    hashtag
    Write vs Read

    Write (.write, .twrite, .dwrite):

    • Broadcasts a transaction

    • Consumes gas

    • Modifies contract state

    • Returns transaction hash

    Read (.read, .tread):

    • Executes an eth_call

    • Free (no gas cost)

    • Does not modify state


    hashtag
    Usage Patterns

    hashtag
    Encrypted Write (.write)

    When to use:

    • Amounts, addresses, or arguments should be private

    • Privacy compliance required

    • Trading, voting, auctions, confidential transfers

    hashtag
    Signed Read (.read)

    When to use:

    • Contract checks msg.sender for access control

    • Caller-specific data (e.g., "my balance")

    • Privacy required for queries

    hashtag
    Transparent Write (.twrite)

    When to use:

    • Data is public anyway

    • Lower gas cost matters

    • No privacy requirements

    • Standard Ethereum behavior

    hashtag
    Transparent Read (.tread)

    When to use:

    • Function is public (doesn't check msg.sender)

    • No authentication needed

    • Data is public

    hashtag
    Debug Write (.dwrite)

    When to use:

    • Development and testing

    • Debugging encryption issues

    • Verifying calldata encoding

    • Auditing transaction parameters


    hashtag
    Common Pitfalls

    hashtag
    Using .tread for Access-Controlled Functions

    Problem:

    Solution:

    hashtag
    Using .write for Public Data

    Unnecessary overhead:

    Better:


    hashtag
    See Also

    • — Contract wrapper reference

    • — Full encryption workflow

    • — Authentication and privacy for reads

    create_wallet_client

    Create sync Web3 instance with full Seismic wallet capabilities

    Create a synchronous Web3 instance with full Seismic wallet capabilities.

    hashtag
    Overview

    create_wallet_client() is the primary factory function for creating a sync client that can perform shielded writes, signed reads, and deposits. It fetches the TEE public key, derives encryption state via ECDHarrow-up-right, and attaches a fully-configured w3.seismic namespace to a standard Web3 instance.

    The returned client works with all standard web3.py APIs (w3.eth.get_block(), w3.eth.send_raw_transaction(), etc.) plus the additional w3.seismic namespace for Seismic-specific operations.

    hashtag
    Signature

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns

    Type
    Description

    hashtag
    Examples

    hashtag
    Basic Usage

    hashtag
    Using Chain Configuration

    hashtag
    With Custom Encryption Key

    hashtag
    Standard Web3 Operations

    hashtag
    How It Works

    The function performs five steps:

    1. Create Web3 instance

    2. Fetch TEE public key (synchronous RPC call)

    3. Generate encryption keypair (if encryption_sk is None, a random ephemeral key is created)

    hashtag
    Client Capabilities

    The returned client provides:

    hashtag
    Standard Web3 Methods (e.g. w3.eth, w3.net)

    • get_block(), get_transaction(), get_balance()

    • send_raw_transaction(), wait_for_transaction_receipt()

    hashtag
    Seismic Methods (w3.seismic)

    • - Send shielded transactions

    • - Debug shielded transactions

    • - Execute signed reads

    hashtag
    Encryption

    The client automatically:

    • Fetches the network's TEE public key

    • Performs ECDH key exchange using encryption_sk (or generates a random one)

    • Derives a shared AES-GCM key via HKDF

    Access the encryption state at w3.seismic.encryption if needed for advanced use cases.

    hashtag
    Notes

    • HTTP only — Sync clients use Web3 with HTTPProvider, which does not support WebSocket connections. This is a limitation of the underlying web3.py library (WebSocketProvider is async-only). If you need WebSocket support (persistent connections, subscriptions), use with ws=True

    • The function makes one synchronous RPC call to fetch the TEE public key

    hashtag
    Warnings

    • Private key security - Never log or expose private keys. Use environment variables or secure key management

    • RPC URL validation - Ensure the RPC URL is correct and accessible

    • Network connectivity - The function will fail if it cannot reach the RPC endpoint

    hashtag
    See Also

    • - Async variant with WebSocket support

    • - Read-only client without private key

    • - Encryption state class

    Deploying

    In this chapter, you'll deploy your ClownBeatdown contract to a local Seismic node for testing. By the end of this guide, you'll have a fully deployed contract that you can interact with using your CLI or scripts. Estimated Time: ~15 minutes.

    hashtag
    Writing the deploy script

    Navigate to the script folder in your project and open the ClownBeatdown.s.sol file located at:

    packages/contracts/script

    and add the following to it:

    // SPDX-License-Identifier: UNLICENSED
    pragma solidity ^0.8.13;
    
    import {Script, console} from "forge-std/Script.sol";
    import {ClownBeatdown} from "../src/ClownBeatdown.sol";
    
    contract ClownBeatdownScript is Script {
        ClownBeatdown public clownBeatdown;
    
        function run() public {
            uint256 deployerPrivateKey = vm.envUint("PRIVKEY");
    
            vm.startBroadcast(deployerPrivateKey);
            clownBeatdown = new ClownBeatdown(3);
            vm.stopBroadcast();
    
            console.log("Deployed at:", address(clownBeatdown));
        }
    }

    This script will deploy a new instance of the ClownBeatdown contract with an initial stamina of 3. We'll add secrets separately in the next step, since addSecret performs shielded writes that need to be sent as on-chain transactions.

    hashtag
    Deploying the contract

    1. In a separate terminal window, run

    in order to spin up a local Seismic node.

    1. In packages/contracts, create a .env file and add the following to it:

    The RPC_URL denotes the port on which sanvil is running and the PRIVKEY is one of the standard sanvil testing private keys.

    1. Now, from packages/contracts, deploy the contract:

    The output will show the deployed contract address (e.g. 0x5FbDB2315678afecb367f032d93F642f64180aa3).

    1. Add secrets to the deployed contract using scast send. Replace <CONTRACT_ADDRESS> with the address from the previous step:

    Your contract should be up and deployed to your local Seismic node with 5 secrets!

    Development Toolkit

    Use sfoundry to write and test smart contract code locally before deployment


    hashtag
    Mappings to foundry

    Seismic's development toolkit closely mirrors Foundryarrow-up-right (it's a forkarrow-up-right!). The mapping is as follows:

    // foundry tool -> seismic version of foundry tool
    forge -> sforge
    anvil -> sanvil
    cast -> scast

    You should use the righthand version of all tools when developing for Seismic to get expected behavior. Our documentation assumes familiarity with foundry.


    hashtag
    Quick actions

    Substitute sforge for forge to execute against Seismic's superset of the EVM. More on this in the next section.


    hashtag
    Local node

    Use sanvil to run a local Seismic node for development and testing:

    This starts a local node at http://localhost:8545 with pre-funded accounts, similar to Foundry's anvil.

    Encryption

    ECDH key exchange, AES-GCM calldata encryption, and the Seismic encryption pipeline

    seismic-viem handles encryption transparently when you call writeContract() or signedCall() on a ShieldedWalletClient. The client derives a shared AES key during construction and encrypts calldata on every shielded operation automatically. This page documents the underlying encryption pipeline for advanced use cases where you need direct access to the cryptographic primitives.

    hashtag
    Encryption Flow

    Shielded Wallet Provider

    React context provider for shielded wallet and public client

    React context component that wraps wagmi's connector client to provide ShieldedPublicClient and ShieldedWalletClient from . This is the core provider that all Seismic React hooks depend on.

    hashtag
    Import

    Contract Instance

    Type-safe shielded contract instances with smart routing and read/write/sread/swrite/tread/twrite/dwrite namespaces

    getShieldedContract creates a type-safe contract instance with seven namespaces for interacting with shielded contracts. It extends viem's getContract with Seismic-specific read and write patterns, giving you a single object that covers smart routing, encrypted writes, signed reads, transparent operations, and debug inspection.

    hashtag
    Constructor

    create_public_client

    Create sync Web3 instance with public (read-only) Seismic access

    Create a synchronous Web3 instance with public (read-only) Seismic access.

    hashtag
    Overview

    create_public_client() creates a client for read-only operations on the Seismic network. No private key is required. The namespace provides only public read operations: , , , and (with .tread

    Yes

    Contract ABI

    functionName

    string

    Yes

    Name of the nonpayable/payable function

    args

    array

    No

    Arguments to pass to the function

    gas

    bigint

    No

    Gas limit override

    gasPrice

    bigint

    No

    Gas price override

    isLoading

    boolean

    Whether a write is in progress

    error

    Error | null

    Error from the most recent write

    hash

    `0x${string}` | null

    Transaction hash from last successful write

    ShieldedWalletProvider
    Hooks Overview
    0x67
    0x68
    useShieldedWriteContract
    useSignedReadContract
    AppKit
    EIP-712
    ABIs
    (full capabilities)
  • Smart Routing -- .read and .write auto-detect shielded parameters and route to the correct path

  • Contract Abstraction -- getShieldedContract provides .read, .write (smart), .sread, .swrite (force shielded), .tread, .twrite (force transparent), and .dwrite methods

  • Automatic Encryption Pipeline -- Calldata encryption handled transparently in the client layer

  • Type 0x4A Transactions -- Native support for Seismic transaction type with custom chain formatters

  • Precompile Bindings -- TypeScript wrappers for all Seismic precompiles (RNG, ECDH, AES-GCM, HKDF, secp256k1)

  • Chain Configs -- Pre-configured chain definitions for testnet, local dev, and custom devnets

  • Full viem Compatibility -- All standard viem client methods work unchanged

  • Send an encrypted transaction -> Shielded Writes
  • Read with identity proof -> Signed Reads

  • Connect to a network -> Chains

  • Understand calldata encryption -> Encryption

  • Install the package -> Installation

  • )
  • Encryption -> Encryption (getEncryption, AesGcmCrypto)

  • Precompiles -> Precompiles (rng, ecdh, aesGcmEncrypt, hkdf, secp256k1Sig)

  • Built directly on viem 2.x

    Built on seismic-viem + wagmi

    Client creation

    Manual (createShieldedWalletClient)

    Automatic via ShieldedWalletProvider

    State management

    Manual

    React hooks (useShieldedWallet, etc.)

    Use when

    Server-side, scripts, non-React apps

    React applications

    Understand encryption -- Learn how calldata encryption works
  • Interact with contracts -- Use getShieldedContract for reads and writes

  • Explore precompiles -- Access on-chain RNG, ECDH, and AES-GCM

  • Installation

    Install seismic-viem, configure viem peer dependency

    Chains

    Pre-configured chain definitions for Seismic networks

    Shielded Public Client

    Read-only client with TEE key exchange and precompile access

    Shielded Wallet Client

    Full-featured client with encryption pipeline and transaction signing

    Contract Instance

    getShieldedContract for .read, .write (smart), .sread, .swrite (force shielded), .tread, .twrite (force transparent), .dwrite

    Shielded Writes

    Encrypt calldata and send Seismic transactions (type 0x4A)

    Signed Reads

    Prove caller identity in eth_call via seismic_call

    Chains

    seismicTestnet, sanvil, createSeismicDevnet chain configs

    Encryption

    ECDH key exchange, AES-GCM calldata encryption

    Precompiles

    RNG, ECDH, AES-GCM, HKDF, secp256k1 signing precompile bindings

    Level

    Low-level SDK

    React hooks layer

    Framework

    Framework-agnostic TypeScript

    React 18+

    Chains
    seismic-react
    Install seismic-viem
    Configure a chain

    Foundation

    # Initializes a project called `Counter`
    sforge init Counter
    # Run tests for the Counter contract
    sforge test
    # Use sforge scripts to deploy the Counter contract
    # Running `sanvil` @ http://localhost:8545
    # Set the private key in the env
    export PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" # Address - 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266
    # Run the script and broadcast the deploy transaction
    sforge script script/Counter.s.sol --rpc-url http://127.0.0.1:8545 --broadcast --private-key $PRIVATE_KEY
    seismic-viem
    ├── Client Layer
    │   ├── createShieldedPublicClient  — read-only, TEE key, precompiles
    │   └── createShieldedWalletClient  — full capabilities, encryption pipeline
    ├── Contract Layer
    │   ├── getShieldedContract          — .read / .write (smart) / .sread / .swrite (force shielded) / .tread / .twrite (force transparent) / .dwrite
    │   ├── shieldedWriteContract        — standalone encrypted write
    │   └── signedReadContract           — standalone signed read
    ├── Chain Configs
    │   ├── seismicTestnet               — public testnet (chain ID 5124)
    │   ├── sanvil                       — local dev (chain ID 31337)
    │   └── createSeismicDevnet          — custom chain factory
    ├── Encryption
    │   ├── getEncryption                — ECDH key exchange → AES key
    │   └── AesGcmCrypto                 — encrypt/decrypt calldata
    └── Precompiles
        ├── rng                          — random number generation
        ├── ecdh                         — key exchange
        ├── aesGcmEncrypt / Decrypt      — on-chain encryption
        ├── hkdf                         — key derivation
        └── secp256k1Sig                 — signature generation
    sanvil
    RPC_URL=http://127.0.0.1:8545
    PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
    source .env
    sforge script script/ClownBeatdown.s.sol:ClownBeatdownScript \
        --rpc-url $RPC_URL \
        --broadcast
    scast send <CONTRACT_ADDRESS> "addSecret(string)" "The moon is made of cheese" \
        --rpc-url $RPC_URL --private-key $PRIVKEY
    scast send <CONTRACT_ADDRESS> "addSecret(string)" "Clowns rule the underworld" \
        --rpc-url $RPC_URL --private-key $PRIVKEY
    scast send <CONTRACT_ADDRESS> "addSecret(string)" "The cake is a lie" \
        --rpc-url $RPC_URL --private-key $PRIVKEY
    scast send <CONTRACT_ADDRESS> "addSecret(string)" "42 is the answer" \
        --rpc-url $RPC_URL --private-key $PRIVKEY
    scast send <CONTRACT_ADDRESS> "addSecret(string)" "Never trust a smiling clown" \
        --rpc-url $RPC_URL --private-key $PRIVKEY
    sanvil

    sbytes -- shielded bytes, both fixed-length (sbytes1 through sbytes32) and dynamic (sbytes)

  • saddress -- shielded address

  • The encrypted transaction is broadcast to the network.
  • Inside the TEE, the node decrypts the calldata, executes the transaction, and writes results to shielded storage.

  • Encrypt data with AES-GCM

    0x67

    Decrypt data with AES-GCM

    0x68

    Derive cryptographic keys from a parent key

    0x69

    Sign a message with a secret key

    State management: Maintains the world state using FlaggedStorage, where each storage slot is tagged as public or private.
  • Transaction pool: Receives both standard Ethereum transactions and Seismic transactions. Encrypted calldata in Seismic transactions is decrypted inside the TEE before execution.

  • seismic-revm-inspectorsarrow-up-right (fork of revm-inspectors): EVM tracing/debugging with Seismic support.

  • seismic-evmarrow-up-right (fork of alloy-evm): Seismic block execution layer.

  • seismic-retharrow-up-right (fork of reth): Full node with Enclave communication, modified RPC (redacting shielded data), and TEE attestation.

  • seismic-solidityarrow-up-right (fork of solidity): Compiler adding shielded types (suint, sbool, sbytes, saddress).

  • seismic-foundryarrow-up-right (fork of foundry): sforge, sanvil, scast dev tools.

  • seismic-compilersarrow-up-right (fork of compilers): Compiler integration for sforge.

  • seismic-foundry-fork-dbarrow-up-right (fork of foundry-fork-db): Fork DB with FlaggedStorage support.

  • Only after successful verification does the new node receive the root key.

    Seismic Solidity compiler

    Adds shielded types (suint, sint, sbool, sbytes, saddress) that compile to privacy-aware opcodes

    Seismic network

    EVM-compatible L1 where nodes run inside Trusted Execution Environments (TEEs)

    Shielded storage (FlaggedStorage)

    Storage model where each slot carries an is_private flag, enforced at the opcode level

    suint256 balance = 100s;
    (value, is_private)
    mapping(address => suint256) balanceOf;
    
    function transfer(address to, suint256 amount) public {
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    }

    0x64

    RNG

    Securely generate a random number

    0x65

    ECDH

    Elliptic Curve Diffie-Hellman -- derive a shared secret from a public key and a private key

    Seismic Retharrow-up-right

    Transaction processing, state management, RPC

    Summitarrow-up-right

    Consensus and block production

    Enclave

    TEE operations: key management, encryption/decryption, attestation

    solidity  alloy-core  alloy-trie  revm  revm-inspectors  alloy-evm  reth   foundry  compilers  foundry-fork-db
       |          |           |         |         |              |        |        |         |            |
     ssolc   seismic-    seismic-  seismic-  seismic-revm-  seismic-  seismic- seismic- seismic-    seismic-
            alloy-core    trie       revm     inspectors      evm      reth   foundry  compilers  foundry-fork-db
    Summitarrow-up-right
    The Seismic Transaction
    Seismic EVMarrow-up-right
    retharrow-up-right
    precompiles
    seismic-alloy-corearrow-up-right
    seismic-triearrow-up-right
    seismic-revmarrow-up-right
    summitarrow-up-right
    seismic-enclavearrow-up-right
    seismic-alloyarrow-up-right
    Repos
    Commonwarearrow-up-right
    Block structure, state tries, and TxSeismic transaction format
    RPC, EVM, and storage interaction diagram showing how shielded slots are protected
    Seismic node architecture showing components inside the TEE boundary

    0x66

    Read

    Yes

    Your address

    No

    Access-controlled reads

    Write

    No

    Your address

    Yes

    Public writes

    Read

    No

    0x0

    No

    Public reads

    Write

    Yes

    Your address

    Yes

    Debug/testing

    Higher gas cost (encryption overhead)

    Faster (no encryption computation)

    Must wait for confirmation

    Returns result immediately
  • No confirmation needed

  • Result should be encrypted
    — Security parameters
  • DebugWriteResult — Debug write return type

  • Namespace

    Operation

    Encryption

    msg.sender

    Broadcasts

    Use Case

    .write

    Write

    Yes

    Your address

    Yes

    # Privacy-sensitive transaction
    tx_hash = contract.write.transfer(recipient, 1000)
    
    # Wait for confirmation
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    # Encrypted read that proves your identity — auto-decoded
    balance = contract.read.balanceOf()  # int
    # Public transaction
    tx_hash = contract.twrite.approve(spender, amount)
    
    # Wait for confirmation
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    # Public read — auto-decoded
    total_supply = contract.tread.totalSupply()  # int
    # Debug transaction with inspection
    result = contract.dwrite.transfer(recipient, 1000)
    
    # Inspect plaintext calldata
    print(f"Plaintext: {result.plaintext_tx.data.to_0x_hex()}")
    print(f"Encrypted: {result.shielded_tx.data.to_0x_hex()}")
    print(f"Tx hash: {result.tx_hash.to_0x_hex()}")
    
    # Wait for confirmation
    receipt = w3.eth.wait_for_transaction_receipt(result.tx_hash)
    # BAD: SRC20 balanceOf uses msg.sender — returns 0x0's balance
    balance = contract.tread.balanceOf()  # 0
    # GOOD: Proves your identity
    balance = contract.read.balanceOf()  # Your actual balance
    # Wasteful: Encrypts already-public data
    tx_hash = contract.write.approve(spender, amount)
    # More efficient: Use transparent write
    tx_hash = contract.twrite.approve(spender, amount)
    Contract Instance
    Shielded Write Guide
    Signed Read Guide

    Privacy-sensitive writes

    SeismicSecurityParams

    32-byte secp256k1 private key for signing transactions

    encryption_sk

    No

    Optional 32-byte key for ECDH. If None, a random ephemeral key is generated

    Derive encryption state (ECDH + HKDFarrow-up-right)

    encryption = get_encryption(network_pk, encryption_sk)
  • Attach Seismic namespace

    w3.seismic = SeismicNamespace(w3, encryption, private_key)
  • All other standard web3.py functionality
    - Deposit ETH/tokens
  • get_tee_public_key() - Get TEE public key

  • get_deposit_root() - Query deposit merkle root

  • get_deposit_count() - Query deposit count

  • contract() - Create contract wrappers

  • Uses this key to encrypt all shielded transaction calldata and signed reads

    If encryption_sk is None, a random ephemeral key is generated

  • The encryption key is separate from the transaction signing key

  • The returned Web3 instance is fully compatible with all web3.py APIs

  • For async operations, use create_async_wallet_client()

  • HTTPS recommended - Use HTTPS URLs in production to prevent MITM attacks
    - Encryption derivation function
  • SeismicNamespace - The w3.seismic namespace

  • Chains Configuration - Pre-configured chain constants

  • def create_wallet_client(
        rpc_url: str,
        private_key: PrivateKey,
        *,
        encryption_sk: PrivateKey | None = None,
    ) -> Web3

    rpc_url

    str

    Yes

    HTTP(S) URL of the Seismic node (e.g., "https://testnet-1.seismictest.net/rpc"). WebSocket URLs are not supported — see note below

    private_key

    PrivateKey

    Web3

    A Web3 instance with w3.seismic namespace attached (SeismicNamespace)

    import os
    from seismic_web3 import create_wallet_client, PrivateKey
    
    # Load private key
    private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
    # Create wallet client
    w3 = create_wallet_client(
        "https://testnet-1.seismictest.net/rpc",
        private_key=private_key,
    )
    
    # Now use w3.seismic for Seismic operations
    contract = w3.seismic.contract(address, abi)
    tx_hash = contract.swrite.transfer(recipient, 1000)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    import os
    from seismic_web3 import SEISMIC_TESTNET, PrivateKey
    
    private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
    # Recommended: use chain config instead of raw URL
    w3 = SEISMIC_TESTNET.wallet_client(private_key)
    
    # Equivalent to:
    # w3 = create_wallet_client(SEISMIC_TESTNET.rpc_url, private_key=private_key)
    import os
    from seismic_web3 import create_wallet_client, PrivateKey
    
    signing_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    encryption_key = PrivateKey(os.urandom(32))  # Custom encryption keypair
    
    w3 = create_wallet_client(
        "https://testnet-1.seismictest.net/rpc",
        private_key=signing_key,
        encryption_sk=encryption_key,
    )
    import os
    from seismic_web3 import create_wallet_client, PrivateKey
    
    private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    w3 = create_wallet_client("https://testnet-1.seismictest.net/rpc", private_key=private_key)
    
    # All standard web3.py operations work
    block = w3.eth.get_block("latest")
    balance = w3.eth.get_balance("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
    chain_id = w3.eth.chain_id
    w3 = Web3(Web3.HTTPProvider(rpc_url))
    network_pk = get_tee_public_key(w3)
    encryption_sk = encryption_sk or PrivateKey(os.urandom(32))
    send_shielded_transaction()
    debug_send_shielded_transaction()
    signed_call()
    create_async_wallet_client()
    create_async_wallet_client
    create_public_client
    EncryptionState

    Yes

    deposit()
    get_encryption
    Every shielded transaction follows this pipeline:
    circle-info

    You don't need to call getEncryption() manually -- createShieldedWalletClient handles key exchange automatically. The functions on this page are for advanced use cases like offline encryption, custom key management, or debugging.

    hashtag
    getEncryption(networkPk, clientSk?)

    Standalone function that performs ECDH key exchange and derives an AES-256 key from a TEE public key and an optional client secret key.

    hashtag
    Import

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    networkPk

    string

    Yes

    TEE's secp256k1 public key (fetched via client.getTeePublicKey())

    clientSk

    Hex

    hashtag
    Returns

    Property
    Type
    Description

    aesKey

    Hex

    Derived AES-256 key for encrypting calldata

    encryptionPrivateKey

    Hex

    Client's ephemeral secp256k1 private key

    hashtag
    Example

    hashtag
    Encryption Actions

    The ShieldedWalletClient exposes encryption operations as client methods. These use the AES key that was derived during client construction.

    Action
    Return Type
    Description

    getEncryption()

    Hex

    Returns the AES-256 key used for shielded operations

    getEncryptionPublicKey()

    Hex

    Returns the client's compressed secp256k1 encryption public key

    hashtag
    Example

    hashtag
    SeismicTxExtras

    Every Seismic transaction includes additional fields that carry encryption metadata alongside the standard Ethereum transaction fields.

    Field
    Type
    Description

    encryptionPubkey

    Hex

    Client's compressed secp256k1 public key

    encryptionNonce

    Hex

    Random 12-byte AES-GCM nonce

    These fields are populated automatically by writeContract() and signedCall(). The node uses the encryptionPubkey to reconstruct the shared secret and decrypt the calldata inside the TEE.

    hashtag
    AES-GCM with AEAD

    Seismic uses AES-GCM with Additional Authenticated Data (AEAD) to bind encrypted calldata to the transaction context. The TxSeismicMetadata is encoded and passed as AAD during encryption, which means:

    • The ciphertext can only be decrypted when presented alongside the correct metadata.

    • Any modification to the transaction's metadata (account, nonce, recipient, value, or SeismicTxExtras) causes decryption to fail.

    • This prevents replay attacks and ensures calldata cannot be reattached to a different transaction context.

    TxSeismicMetadata includes the following fields as AAD:

    • account -- sender address

    • nonce -- transaction nonce

    • to -- recipient address

    • value -- ETH value

    • seismicElements -- the encryptionPubkey, encryptionNonce, messageVersion, recentBlockHash, expiresAtBlock, and signedRead fields

    hashtag
    Crypto Dependencies

    seismic-viem uses the following @noble libraries for client-side cryptography. These are bundled as direct dependencies and do not need to be installed separately.

    Package
    Purpose

    @noble/curves

    secp256k1 keypair generation and ECDH

    @noble/ciphers

    AES-GCM encryption and decryption

    @noble/hashes

    SHA-256, HKDF, and other hash functions

    circle-info

    All cryptographic operations happen client-side before the transaction is submitted. The plaintext calldata never leaves the client -- only the AES-GCM ciphertext is sent to the node, where it is decrypted inside the TEE for execution.

    hashtag
    See Also

    • Shielded Wallet Client -- Client that performs encryption automatically

    • Shielded Writes -- How encrypted transactions are built and sent

    • Signed Reads -- Authenticated reads that prove caller identity

    • -- On-chain cryptographic operations via precompiled contracts

    • -- Bundled crypto dependencies

    1. Client generates an ephemeral secp256k1 keypair (or uses a provided one)
    2. Client fetches the TEE public key from the node via seismic_getTeePublicKey
    3. ECDH(client_sk, tee_pk) → shared secret
    4. Shared secret → AES-256 key (via key derivation)
    5. For each transaction:
       a. Generate a random 12-byte nonce
       b. Encode TxSeismicMetadata as Additional Authenticated Data (AAD)
       c. AES-GCM encrypt(plaintext_calldata, nonce, aad) → ciphertext
       d. Include encryptionPubkey + nonce in the transaction's SeismicTxExtras fields
    import { getEncryption } from "seismic-viem";
    import { createShieldedPublicClient, getEncryption } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    
    // Fetch the TEE public key from the node
    const publicClient = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    
    const teePublicKey = await publicClient.getTeePublicKey();
    
    // Derive encryption keys without constructing a full wallet client
    const encryption = getEncryption(teePublicKey);
    console.log("AES key:", encryption.aesKey);
    console.log("Encryption public key:", encryption.encryptionPublicKey);
    console.log("Encryption private key:", encryption.encryptionPrivateKey);
    
    // Or provide your own private key for deterministic key derivation
    const deterministicEncryption = getEncryption(
      teePublicKey,
      "0xYourSecp256k1PrivateKey",
    );
    import { createShieldedWalletClient } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    
    const walletClient = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    
    // Access the derived AES key
    const aesKey = walletClient.getEncryption();
    console.log("AES key:", aesKey);
    
    // Access the encryption public key
    const encPubKey = walletClient.getEncryptionPublicKey();
    console.log("Encryption public key:", encPubKey);
    hashtag
    Props
    Prop
    Type
    Required
    Description

    children

    React.ReactNode

    Yes

    Child components that can access shielded wallet context

    config

    Config (from wagmi)

    hashtag
    OnAddressChangeParams

    hashtag
    Context Value

    The provider exposes a WalletClientContextType to all descendant components via React context:

    Field
    Description

    publicClient

    Shielded public client for encrypted reads. null until initialization completes.

    walletClient

    Shielded wallet client for encrypted writes. null until a wallet is connected.

    address

    Connected wallet address. null when no wallet is connected.

    circle-info

    Access these values through the useShieldedWallet hook rather than consuming the context directly.

    hashtag
    Setup

    hashtag
    Basic Setup with RainbowKit

    hashtag
    Next.js Setup

    For Next.js, create the provider in a client component:

    Then wrap your layout:

    hashtag
    Provider Nesting Order

    The providers must be nested in this order:

    circle-exclamation

    ShieldedWalletProvider must be inside WagmiProvider because it reads the connected wallet from wagmi's context. Placing it outside will cause a runtime error.

    hashtag
    Lifecycle

    The provider follows this initialization sequence:

    1. Public client creation -- On mount, creates a ShieldedPublicClient using the chain and transport from the wagmi config (or from options.publicChain / options.publicTransport if provided). This client is available even when no wallet is connected.

    2. Wallet client creation -- When a wallet connects through wagmi, the provider creates a ShieldedWalletClient from the connector's client. This enables encrypted write transactions.

    3. onAddressChange callback -- If provided, called after both clients are ready with the new address. Use this to load user-specific data or initialize contract state.

    4. Reconnection handling -- When the user switches accounts or reconnects, the provider recreates the wallet client and fires onAddressChange again.

    hashtag
    Usage with onAddressChange

    The onAddressChange callback is useful for initializing state when a wallet connects:

    hashtag
    Error Handling

    The provider captures errors during client creation and exposes them through the context:

    Error State
    Cause
    Resolution

    Public client creation fails

    Invalid chain config or transport

    Check wagmi config and chain definitions

    Wallet client creation fails

    Connector incompatibility or network mismatch

    Ensure the wallet supports the target chain

    Access the error state through useShieldedWallet:

    hashtag
    See Also

    • Installation -- Package setup and peer dependencies

    • useShieldedWallet -- Hook to consume the provider context

    • Hooks Overview -- All available hooks

    • -- Wallet UI integration

    • -- Embedded wallet setup

    import { ShieldedWalletProvider } from "seismic-react";
    seismic-viemarrow-up-right
    type OnAddressChangeParams = {
      publicClient: ShieldedPublicClient;
      walletClient: ShieldedWalletClient;
      address: Hex;
    };
    interface WalletClientContextType {
      publicClient: ShieldedPublicClient | null;
      walletClient: ShieldedWalletClient | null;
      address: Hex | null;
      error: string | null;
      loaded: boolean;
    }
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit'
    import { ShieldedWalletProvider } from 'seismic-react'
    import { seismicTestnet } from 'seismic-react/rainbowkit'
    
    const config = getDefaultConfig({
      appName: 'My Seismic App',
      projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
      chains: [seismicTestnet],
    })
    
    const queryClient = new QueryClient()
    
    export default function App({ children }: { children: React.ReactNode }) {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                {children}
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    'use client'
    
    import { WagmiProvider } from 'wagmi'
    import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
    import { RainbowKitProvider, getDefaultConfig } from '@rainbow-me/rainbowkit'
    import { ShieldedWalletProvider } from 'seismic-react'
    import { seismicTestnet } from 'seismic-react/rainbowkit'
    
    const config = getDefaultConfig({
      appName: 'My Seismic App',
      projectId: 'YOUR_WALLETCONNECT_PROJECT_ID',
      chains: [seismicTestnet],
    })
    
    const queryClient = new QueryClient()
    
    export function Providers({ children }: { children: React.ReactNode }) {
      return (
        <WagmiProvider config={config}>
          <QueryClientProvider client={queryClient}>
            <RainbowKitProvider>
              <ShieldedWalletProvider config={config}>
                {children}
              </ShieldedWalletProvider>
            </RainbowKitProvider>
          </QueryClientProvider>
        </WagmiProvider>
      )
    }
    // app/layout.tsx
    import { Providers } from './providers'
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        <html>
          <body>
            <Providers>{children}</Providers>
          </body>
        </html>
      )
    }
    WagmiProvider
      └─ QueryClientProvider
           └─ RainbowKitProvider
                └─ ShieldedWalletProvider
                     └─ Your App
    import { ShieldedWalletProvider } from 'seismic-react'
    
    function App() {
      const handleAddressChange = async ({
        publicClient,
        walletClient,
        address,
      }) => {
        console.log('Connected:', address)
    
        // Example: read user's shielded balance on connect
        const balance = await publicClient.readContract({
          address: TOKEN_ADDRESS,
          abi: tokenAbi,
          functionName: 'balanceOf',
          args: [address],
        })
        console.log('Balance:', balance)
      }
    
      return (
        <ShieldedWalletProvider
          config={config}
          options={{ onAddressChange: handleAddressChange }}
        >
          <YourApp />
        </ShieldedWalletProvider>
      )
    }
    import { useShieldedWallet } from 'seismic-react'
    
    function MyComponent() {
      const { error, loaded } = useShieldedWallet()
    
      if (!loaded) return <div>Loading...</div>
      if (error) return <div>Error: {error}</div>
    
      return <div>Connected</div>
    }
    Parameter
    Type
    Required
    Description

    abi

    Abi

    Yes

    Contract ABI (use as const for full type inference)

    address

    Address

    Yes

    Deployed contract address

    client

    ShieldedWalletClient | keyed client

    Yes

    hashtag
    Namespaces

    The returned ShieldedContract exposes seven namespaces. Each namespace provides every function defined in the ABI, but differs in how calldata is handled and who appears as msg.sender on-chain.

    Namespace
    Operation Type
    Calldata
    msg.sender
    Description

    .read

    Smart Read

    Auto-detected

    Auto-detected

    Inspects ABI -- shielded if shielded params, else transparent


    hashtag
    .read / .write -- Smart Routing

    The default .read and .write namespaces auto-detect whether a function has shielded parameters (suint*, sint*, sbool, saddress, sbytes*, including nested in tuples and arrays). If any input parameter is shielded, the call is routed to the shielded path; otherwise it uses the transparent path.

    circle-info

    Smart routing inspects input parameters only. If a function has no shielded inputs but you still want an encrypted read (e.g., because the return value is sensitive), use .sread instead.


    hashtag
    .sread -- Force Signed Read

    Always sends an encrypted, signed eth_call that proves your identity to the contract, regardless of whether the function has shielded parameters. Use this when you want the response encrypted or when the contract checks msg.sender in a view function with no shielded inputs.

    circle-info

    The .sread namespace always sets account to client.account for security. This ensures the signed read is authenticated with the wallet's address, regardless of any account override you pass.

    See Signed Reads for details on how signed reads work under the hood.


    hashtag
    .swrite -- Force Shielded Write

    Always encrypts calldata before broadcasting the transaction, regardless of whether the function has shielded parameters. The calldata is not visible on-chain.

    See Shielded Writes for the encryption lifecycle and security parameters.


    hashtag
    .tread -- Transparent Read

    Standard read call with plaintext calldata. The from address is set to the zero address, so msg.sender inside the contract will be 0x0000...0000. Works with both standard and shielded-typed functions (the ABI types are remapped automatically for encoding).

    Use .tread for public view functions that do not depend on msg.sender.

    circle-exclamation

    .tread rejects the account option and will throw. Seismic zeroes out from on transparent eth_call, so an account passed here would be ignored on the node and cause silent bugs. Use .sread for sender-aware reads.


    hashtag
    .twrite -- Transparent Write

    Standard write call with plaintext (unencrypted) calldata. The transaction is signed by the wallet, so msg.sender is the signer's address. Works with both standard and shielded-typed functions (the ABI types are remapped automatically for encoding).

    Use .twrite when you intentionally want calldata to be visible on-chain (e.g., for transparency or debugging).


    hashtag
    .dwrite -- Send + Inspect

    Sends the same encrypted transaction as .swrite -- the tx is broadcast and txHash is a real on-chain hash -- and additionally returns the plaintext transaction view and the shielded (encrypted) transaction view alongside the hash. Useful for inspecting exactly what the SDK encrypted and submitted.

    circle-info

    Despite the "debug" flavor of the name, .dwrite broadcasts a real shielded transaction. If you just want to see what would be sent without submitting, build the calldata yourself via getPlaintextCalldata.

    hashtag
    TypeScript ABI Typing

    For full type inference on function names, argument types, and return types, declare your ABI with as const:

    Without as const, the ABI is widened to readonly AbiItem[] and the contract loses per-function type safety. You can still call functions by name, but arguments and return values will be untyped.

    hashtag
    See Also

    • Shielded Writes -- Encryption lifecycle and shieldedWriteContract

    • Signed Reads -- Authenticated reads and signedReadContract

    • Shielded Wallet Client -- Creating the client passed to getShieldedContract

    • -- ECDH key exchange and AES-GCM calldata encryption

    import { getShieldedContract } from "seismic-viem";
    import { getShieldedContract } from "seismic-viem";
    
    const abi = [
      {
        name: "balanceOf",
        type: "function",
        stateMutability: "view",
        inputs: [{ name: "account", type: "saddress" }],
        outputs: [{ name: "", type: "uint256" }],
      },
      {
        name: "transfer",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [
          { name: "to", type: "saddress" },
          { name: "amount", type: "suint256" },
        ],
        outputs: [{ name: "", type: "bool" }],
      },
      {
        name: "totalSupply",
        type: "function",
        stateMutability: "view",
        inputs: [],
        outputs: [{ name: "", type: "uint256" }],
      },
      {
        name: "approve",
        type: "function",
        stateMutability: "nonpayable",
        inputs: [
          { name: "spender", type: "saddress" },
          { name: "amount", type: "suint256" },
        ],
        outputs: [{ name: "", type: "bool" }],
      },
    ] as const;
    
    const contract = getShieldedContract({
      abi,
      address: "0x1234567890abcdef1234567890abcdef12345678",
      client,
    });
    // transfer() has saddress and suint256 params → shielded write (type 0x4A)
    const hash = await contract.write.transfer(["0x1234...", 100n]);
    
    // totalSupply() has no shielded params → transparent read
    const supply = await contract.read.totalSupply();
    
    // balanceOf() has saddress param → signed read (encrypted, proves identity)
    const balance = await contract.read.balanceOf(["0x1234..."]);
    // Force signed read even though totalSupply() has no shielded params
    const supply = await contract.sread.totalSupply();
    // Force shielded write even though approve() might not need encryption
    const hash = await contract.swrite.approve(["0x1234...", 100n], {
      gas: 100_000n,
    });
    const supply = await contract.tread.totalSupply();
    const hash = await contract.twrite.approve(["0x1234...", 100n]);
    const { plaintextTx, shieldedTx, txHash } = await contract.dwrite.transfer([
      "0x1234...",
      100n,
    ]);
    
    console.log("Plaintext calldata:", plaintextTx.data);
    console.log("Encrypted calldata:", shieldedTx.data);
    console.log("Transaction hash:", txHash);
    const abi = [
      {
        name: "balanceOf",
        type: "function",
        stateMutability: "view",
        inputs: [{ name: "account", type: "address" }],
        outputs: [{ name: "", type: "uint256" }],
      },
    ] as const;
    
    const contract = getShieldedContract({ abi, address: "0x...", client });
    
    // TypeScript knows:
    //   - contract.read.balanceOf exists
    //   - first arg is [Address]
    //   - return type is bigint
    const balance = await contract.read.balanceOf(["0x1234..."]);
    only).

    This is useful for applications that only need to query chain state without submitting transactions, such as block explorers, analytics dashboards, or read-only dApps.

    hashtag
    Signature

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    rpc_url

    str

    Yes

    HTTP(S) URL of the Seismic node (e.g., "https://testnet-1.seismictest.net/rpc"). WebSocket URLs are not supported — see note below

    hashtag
    Returns

    Type
    Description

    Web3

    A Web3 instance with w3.seismic namespace attached ()

    hashtag
    Examples

    hashtag
    Basic Usage

    hashtag
    Using Chain Configuration

    hashtag
    Read-Only Contract Access

    hashtag
    Standard Web3 Operations

    hashtag
    Block Explorer Pattern

    hashtag
    How It Works

    The function performs two steps:

    1. Create Web3 instance

      w3 = Web3(Web3.HTTPProvider(rpc_url))
    2. Attach public Seismic namespace

      w3.seismic = SeismicPublicNamespace(w3)

    No TEE public key fetching or encryption setup is performed since the client cannot perform shielded operations.

    hashtag
    Client Capabilities

    hashtag
    Standard Web3 Methods (e.g. w3.eth, w3.net)

    • get_block(), get_transaction(), get_balance()

    • get_code(), call(), estimate_gas()

    • All other standard read-only web3.py functionality

    hashtag
    Public Seismic Methods (w3.seismic)

    • get_tee_public_key() - Get TEE public key

    • get_deposit_root() - Query deposit merkle root

    • get_deposit_count() - Query deposit count

    • contract() - Create contract wrappers (.tread only)

    hashtag
    NOT Available

    • send_shielded_transaction() - Requires private key

    • debug_send_shielded_transaction() - Requires private key

    • signed_call() - Requires private key and encryption

    • - Requires private key

    • Contract .swrite and .sread methods - Require private key

    hashtag
    Public vs Wallet Client

    Feature
    Public Client
    Wallet Client

    Private key

    Not required

    Required

    Shielded writes

    No

    Yes

    hashtag
    Notes

    • HTTP only — Sync clients use Web3 with HTTPProvider, which does not support WebSocket connections. This is a limitation of the underlying web3.py library (WebSocketProvider is async-only). If you need WebSocket support (persistent connections, subscriptions), use create_async_public_client() with ws=True

    • No private key required or accepted

    • No encryption setup performed

    • No RPC calls during client creation (lightweight)

    • Cannot perform any write operations or shielded reads

    • Contract wrappers only expose .tread (transparent read)

    • For write operations, use

    • For async operations, use

    hashtag
    Use Cases

    • Block explorers and chain analytics

    • Read-only dApps that display public data

    • Monitoring and alerting systems

    • Price oracles and data aggregators

    • Public dashboards and visualizations

    • Testing and validation tools

    hashtag
    See Also

    • create_async_public_client - Async variant with WebSocket support

    • create_wallet_client - Full-featured client with private key

    • SeismicPublicNamespace - The public w3.seismic namespace

    • - Pre-configured chain constants

    • - Working with contract wrappers

    w3.seismic
    get_tee_public_key()
    get_deposit_root()
    get_deposit_count()
    contract()
    def create_public_client(rpc_url: str) -> Web3
    from seismic_web3 import create_public_client
    
    # Create public client
    w3 = create_public_client("https://testnet-1.seismictest.net/rpc")
    
    # Query TEE public key
    tee_pk = w3.seismic.get_tee_public_key()
    print(f"TEE public key: {tee_pk.to_0x_hex()}")
    
    # Query deposit info
    root = w3.seismic.get_deposit_root()
    count = w3.seismic.get_deposit_count()
    print(f"Deposit root: {root.to_0x_hex()}, count: {count}")
    from seismic_web3 import SEISMIC_TESTNET
    
    # Recommended: use chain config instead of raw URL
    w3 = SEISMIC_TESTNET.public_client()
    
    # Equivalent to:
    # w3 = create_public_client(SEISMIC_TESTNET.rpc_url)
    from seismic_web3 import create_public_client
    
    w3 = create_public_client("https://testnet-1.seismictest.net/rpc")
    
    # Create contract wrapper (read-only)
    contract = w3.seismic.contract(
        address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
        abi=contract_abi,
    )
    
    # Only transparent reads are available
    result = contract.tread.balanceOf("0x1234...")
    
    # Shielded operations are NOT available
    # contract.swrite.transfer(...)  # AttributeError: no swrite on public client
    # contract.sread.getBalance(...)  # AttributeError: no sread on public client
    from seismic_web3 import create_public_client
    
    w3 = create_public_client("https://testnet-1.seismictest.net/rpc")
    
    # All standard web3.py read operations work
    block = w3.eth.get_block("latest")
    print(f"Latest block: {block['number']}")
    
    balance = w3.eth.get_balance("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
    print(f"Balance: {w3.from_wei(balance, 'ether')} ETH")
    
    chain_id = w3.eth.chain_id
    print(f"Chain ID: {chain_id}")
    from seismic_web3 import create_public_client
    
    def get_chain_stats(rpc_url: str):
        w3 = create_public_client(rpc_url)
    
        # Get latest block
        block = w3.eth.get_block("latest")
    
        # Get deposit info
        deposit_root = w3.seismic.get_deposit_root()
        deposit_count = w3.seismic.get_deposit_count()
    
        # Get TEE info
        tee_pk = w3.seismic.get_tee_public_key()
    
        return {
            "block_number": block["number"],
            "block_hash": block["hash"].to_0x_hex(),
            "deposit_root": deposit_root.to_0x_hex(),
            "deposit_count": deposit_count,
            "tee_public_key": tee_pk.to_0x_hex(),
        }
    
    stats = get_chain_stats("https://testnet-1.seismictest.net/rpc")
    print(stats)

    ShieldedContract

    Sync contract wrapper with shielded and transparent namespaces

    Synchronous contract wrapper providing encrypted and transparent interaction with Seismic contracts.

    hashtag
    Overview

    ShieldedContract is the primary sync interface for interacting with Seismic smart contracts. It provides five namespaces for different interaction modes: encrypted writes (.write), encrypted reads (.read), transparent writes (.twrite), transparent reads (.tread), and debug writes (.dwrite). Each namespace dynamically exposes contract methods based on the provided ABI.

    hashtag
    Definition

    hashtag
    Constructor Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Namespaces

    hashtag
    .write - Encrypted Write

    Sends encrypted transactions using TxSeismic (type 0x4a). Calldata is encrypted before broadcast.

    Returns: HexBytes (transaction hash)

    Positional Arguments: *args - ABI function arguments (e.g. contract.write.transfer(to, amount))

    Optional Parameters:

    • value: int - Wei to send (default: 0)

    • gas: int | None - Gas limit (default: 30_000_000 when omitted)

    hashtag
    .read - Encrypted Read

    Executes encrypted signed eth_call with encrypted calldata. Result is decrypted and ABI-decoded by the SDK. Single-output functions return the value directly (e.g. int, bool); multi-output functions return a tuple.

    Returns: Any (ABI-decoded Python value)

    Positional Arguments: *args - ABI function arguments (e.g. contract.read.balanceOf(owner))

    Optional Parameters:

    • value: int - Wei for call context (default: 0)

    • gas: int - Gas limit (default: 30_000_000)

    hashtag
    .twrite - Transparent Write

    Sends standard eth_sendTransaction with unencrypted calldata.

    Returns: HexBytes (transaction hash)

    Positional Arguments: *args - ABI function arguments

    Optional Parameters:

    • value: int - Wei to send (default: 0)

    • **tx_params: Any - Additional transaction parameters (gas, gasPrice, etc.)

    hashtag
    .tread - Transparent Read

    Executes standard eth_call with unencrypted calldata. Result is ABI-decoded by the SDK. Single-output functions return the value directly; multi-output functions return a tuple.

    Returns: Any (ABI-decoded Python value)

    Positional Arguments: *args - ABI function arguments

    hashtag
    .dwrite - Debug Write

    Like .write but returns debug information including plaintext and encrypted views. Transaction is actually broadcast.

    Returns:

    Positional Arguments: *args - ABI function arguments

    Optional Parameters: Same as .write

    hashtag
    Examples

    hashtag
    Basic Encrypted Write

    hashtag
    Encrypted Read

    hashtag
    Transparent Operations

    hashtag
    Debug Write

    hashtag
    With Transaction Parameters

    hashtag
    Using EIP-712 Signing

    hashtag
    Instantiation via Client

    hashtag
    Notes

    • Dynamic method access: Contract methods are accessed via __getattr__, so contract.write.setNumber() dynamically resolves to the ABI function

    • ABI remapping: Shielded types (suint256, sbool, saddress) are remapped to standard types for encoding while preserving original names for selector computation

    hashtag
    See Also

    • - Async version of this class

    • - Read-only contract wrapper (no encryption)

    • - Manages encryption keys

    Signed Reads

    Let users check their own balance without exposing it to others

    The SRC20 contract stores balances as suint256, which means there is no public getter. This chapter shows how to let users query their own balance securely using signed reads. Estimated time: ~15 minutes.

    hashtag
    The problem

    In a standard ERC20, balanceOf is a public mapping. Anyone can call balanceOf(address) and see any account's balance. In the SRC20, two things prevent this:

    1. Shielded types cannot be public. The suint256 value type means the automatic getter would try to return a shielded type from an external function, which the compiler rejects.

    2. Vanilla eth_call has no sender identity. On Seismic, the from field is zeroed out for unsigned eth_call

    So you need two things: a way for the contract to verify who is asking, and a way to return the value only to that person.

    hashtag
    Signed reads

    A signed read solves both problems. It is a Seismic transaction (type 0x4A) sent to the eth_call RPC endpoint instead of eth_sendRawTransaction. Because it is a valid signed transaction:

    • The from field is cryptographically verified, so the contract can trust msg.sender.

    • The response is encrypted to the sender's encryption public key (included in the Seismic transaction's elements). Even if someone intercepts the response, they cannot decrypt it.

    • The signed_read

    From the contract's perspective, a signed read looks like a normal view function call. The difference is entirely at the transport layer.

    hashtag
    Contract implementation

    Add a balanceOf function that requires the caller to be the account owner:

    circle-info

    Note the naming here. The mapping balanceOf is internal (no public modifier), so there is no collision with the function name. The function acts as the explicit getter that replaces the auto-generated one.

    This function does three things:

    1. Checks msg.sender -- Only the account owner can query their own balance. With a vanilla eth_call, msg.sender would be address(0) and this check would always fail. With a signed read, msg.sender is the actual caller.

    2. Casts to

    hashtag
    Allowance checking

    The same pattern applies to allowances:

    Here, either the owner or the spender can check the allowance. Both parties have a legitimate reason to know the value.

    hashtag
    Client-side code

    On the client side, seismic-viem handles signed reads automatically under the hood. When you use a ShieldedWalletClient or a ShieldedContract, read calls are sent as signed reads. If you need an unsigned read (a vanilla eth_call), use contract.tread instead.

    hashtag
    Using a shielded contract instance

    The token.read.balanceOf() call is sent as a signed read under the hood. The wallet client signs the request, the node verifies the signature, executes the view function with the correct msg.sender, and encrypts the response to the caller's key.

    hashtag
    Using the wallet client directly

    You can also call readContract on the wallet client:

    Both approaches produce the same signed read.

    hashtag
    Security model

    The signed read has several layers of protection:

    Property
    How it works

    The result is that only the account owner can view their balance, and the balance is never exposed in plaintext outside the TEE.

    hashtag
    A note on privacy tradeoffs

    With this implementation, each user can only see their own balance. They cannot see anyone else's balance. This is the strictest privacy model. In the next chapter, , we will add authorized roles (such as compliance officers) who can view specific balances when required -- without compromising privacy for everyone else.

    Shielded Public Client

    Read-only client with TEE key access and precompile support

    Read-only client for interacting with a Seismic node. Extends viem's PublicClient with TEE public key retrieval, precompile access, block explorer URL helpers, SRC20 event watching, and deposit contract queries. Does not require a private key.

    hashtag
    Import

    AsyncShieldedContract

    Async contract wrapper with shielded and transparent namespaces

    Asynchronous contract wrapper providing encrypted and transparent interaction with Seismic contracts.

    hashtag
    Overview

    AsyncShieldedContract is the async version of ShieldedContract, providing the same five namespaces (.write, .read

    No

    Client's encryption private key. If omitted, generates a random keypair

    encryptionPublicKey

    Hex

    Client's compressed secp256k1 public key

    encrypt(plaintext, metadata)

    Promise<Hex>

    AES-GCM encrypt plaintext calldata with metadata as AAD

    decrypt(ciphertext, metadata)

    Promise<Hex>

    AES-GCM decrypt ciphertext using metadata as AAD

    messageVersion

    number

    0 for normal transactions, 2 for EIP-712 signed

    recentBlockHash

    Hex

    Recent block hash used for replay protection

    expiresAtBlock

    bigint

    Block number after which the transaction expires

    signedRead

    boolean

    true for signed reads, false for standard writes

    Precompiles
    Installation

    Yes

    wagmi configuration object

    options

    object

    No

    Additional configuration options

    options.publicTransport

    Transport

    No

    Custom transport for the public client

    options.publicChain

    Chain

    No

    Custom chain for the public client

    options.onAddressChange

    (params: OnAddressChangeParams) => Promise<void>

    No

    Callback fired when the connected wallet address changes

    error

    Error message if client creation fails. null when healthy.

    loaded

    true once the provider has finished its initial setup, regardless of whether a wallet is connected.

    onAddressChange throws

    Error in your callback

    The error is caught and set on context.error

    RainbowKit Guide
    Privy Guide

    Wallet client for full capabilities, or a keyed client (e.g., { public: publicClient }) for read-only use. All writes (.write, .swrite, .twrite, .dwrite) and signed reads (.sread, plus .read when the target function has shielded params) require a wallet client. A keyed read-only client only supports .tread and .read on functions without shielded params.

    .write

    Smart Write

    Auto-detected

    Signer's address

    Inspects ABI -- shielded if shielded params, else transparent

    .sread

    Force Signed Read

    Encrypted

    Signer's address

    Always authenticated read -- proves identity

    .swrite

    Force Shielded

    Encrypted

    Signer's address

    Always encrypted transaction

    .tread

    Transparent Read

    Plaintext

    Zero address

    Always standard read -- rejects account

    .twrite

    Transparent Write

    Plaintext

    Signer's address

    Always standard write

    .dwrite

    Send + Inspect

    Encrypted

    Signer's address

    Broadcasts shielded tx and returns plaintext + encrypted tx + hash

    Encryption
    AES-GCM Encrypt
    AES-GCM Decrypt
    HKDF
    secp256k1 Sign
    .read
    .twrite
    .tread
    .dwrite
    PrivateKey

    Signed reads

    No

    Yes

    Transparent reads

    Yes

    Yes

    Deposits

    No

    Yes

    TEE queries

    Yes

    Yes

    Standard Web3

    All read operations

    All operations

    deposit()
    create_wallet_client()
    create_async_public_client()
    Chains Configuration
    Contract Instances
    SeismicPublicNamespace
    requests. This means a contract cannot verify
    msg.sender
    in a view function called via a normal
    eth_call
    --
    msg.sender
    would just be
    address(0)
    . Without sender verification, anyone could impersonate any address and read their balance.
    flag in SeismicElements should be set to
    true
    , in order to prevent anyone from replaying this payload as a write transaction.
    uint256
    -- The shielded
    suint256
    value is cast to a regular
    uint256
    for the return value. Shielded types cannot be returned from external functions.
  • Returns the balance -- The return value is encrypted to the caller's key by the Seismic node before being sent back. Even though the function returns a uint256, the response is encrypted because the call was made with a signed read.

  • A signed read is sent to eth_call, which does not modify state. Even if the replay protection were bypassed, the function is view and cannot alter balances.

    Sender authentication

    The transaction is signed by the user's private key. The node verifies the signature before executing.

    Response encryption

    The response is encrypted to the user's encryption public key, included in the SeismicElements of the transaction.

    Replay protection

    The signed_read flag is set to true. If someone intercepts the signed read payload and submits it to eth_sendRawTransaction, it is rejected.

    Intelligence Contracts

    No state changes

    function balanceOf(address account) external view returns (uint256) {
        require(msg.sender == account, "Only owner can view balance");
        return uint256(balanceOf[account]);
    }
    function allowanceOf(address owner, address spender) external view returns (uint256) {
        require(
            msg.sender == owner || msg.sender == spender,
            "Not authorized"
        );
        return uint256(allowance[owner][spender]);
    }
    import { createShieldedWalletClient, getShieldedContract, seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    
    // Create a shielded wallet client
    const walletClient = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http("https://testnet-1.seismictest.net/rpc"),
      account: privateKeyToAccount(PRIVATE_KEY),
    });
    
    // Get a contract instance
    const token = getShieldedContract({
      abi: src20Abi,
      address: SRC20_ADDRESS,
      client: walletClient,
    });
    
    // Read balance -- this is automatically a signed read
    const balance = await token.read.balanceOf([walletClient.account.address]);
    console.log("My balance:", balance);
    const balance = await walletClient.readContract({
      address: SRC20_ADDRESS,
      abi: src20Abi,
      functionName: "balanceOf",
      args: [walletClient.account.address],
    });

    Encryption state for shielded operations

    private_key

    Yes

    32-byte secp256k1 private key for signing transactions

    address

    ChecksumAddress

    Yes

    Contract address (checksummed Ethereum address)

    abi

    list[dict[str, Any]]

    Yes

    Contract ABI (list of function entries)

    eip712

    bool

    No

    Use EIP-712 typed data signing (default: False)

    gas_price: int | None - Gas price in wei (default: network suggested)
  • security: [SeismicSecurityParams](../api-reference/transaction-types/seismic-security-params.md) | None - Security parameters for expiry

  • security: [SeismicSecurityParams](../api-reference/transaction-types/seismic-security-params.md) | None - Security parameters for expiry

    Gas defaults: .write uses 30_000_000 when gas is omitted; .twrite follows normal web3.py/provider transaction behavior

  • Encryption overhead: Encrypted operations add ~16 bytes (AES-GCM auth tag) to calldata

  • EIP-712 vs raw: EIP-712 signing enables integration with browser extension wallets like Metamask; raw signing is faster for automation and likely what you want to use in this Python SDK

  • Use .write in production: .dwrite is for debugging only

  • - Debug write return type
  • SeismicSecurityParams - Transaction expiry parameters

  • Contract Namespaces - Detailed namespace documentation

  • Shielded Write Guide - Complete workflow guide

  • class ShieldedContract:
        def __init__(
            self,
            w3: Web3,
            encryption: EncryptionState,
            private_key: PrivateKey,
            address: ChecksumAddress,
            abi: list[dict[str, Any]],
            eip712: bool = False,
        ) -> None:
            ...

    w3

    Web3

    Yes

    Synchronous Web3 instance connected to Seismic RPC

    encryption

    EncryptionState

    from seismic_web3 import create_wallet_client, ShieldedContract
    
    w3 = create_wallet_client(
        rpc_url="https://testnet-1.seismictest.net/rpc",
        private_key=private_key,
    )
    
    contract = ShieldedContract(
        w3=w3,
        encryption=w3.seismic.encryption,
        private_key=private_key,
        address="0x1234567890123456789012345678901234567890",
        abi=CONTRACT_ABI,
    )
    
    # Encrypted write - calldata hidden on-chain
    tx_hash = contract.write.setNumber(42)
    print(f"Transaction: {tx_hash.to_0x_hex()}")
    
    # Wait for confirmation
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    print(f"Status: {receipt['status']}")
    # Encrypted read — calldata and result hidden, auto-decoded
    number = contract.read.getNumber()  # int
    print(f"Number: {number}")
    # Transparent write - calldata visible on-chain
    tx_hash = contract.twrite.setNumber(42)
    receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
    
    # Transparent read — standard eth_call, auto-decoded
    number = contract.tread.getNumber()
    print(f"Result: {number}")
    # Debug write - returns plaintext and encrypted views
    debug_result = contract.dwrite.transfer("0xRecipient...", 1000)
    
    print(f"Transaction hash: {debug_result.tx_hash.to_0x_hex()}")
    print(f"Plaintext data: {debug_result.plaintext_tx.data.to_0x_hex()}")
    print(f"Encrypted data: {debug_result.shielded_tx.data.to_0x_hex()}")
    
    # Transaction is actually broadcast
    receipt = w3.eth.wait_for_transaction_receipt(debug_result.tx_hash)
    # Custom gas and value
    tx_hash = contract.write.deposit(
        value=10**18,  # 1 ETH
        gas=200_000,
        gas_price=20 * 10**9,  # 20 gwei
    )
    
    # With security parameters
    from seismic_web3.transaction_types import SeismicSecurityParams
    
    security = SeismicSecurityParams(blocks_window=100)
    tx_hash = contract.write.withdraw(
        amount,
        security=security,
    )
    # Enable EIP-712 for typed data signing
    contract = ShieldedContract(
        w3=w3,
        encryption=w3.seismic.encryption,
        private_key=private_key,
        address=contract_address,
        abi=CONTRACT_ABI,
        eip712=True,  # Use EIP-712 instead of raw signing
    )
    
    tx_hash = contract.write.setNumber(123)
    # Most common pattern - let the client create the contract
    from seismic_web3 import create_wallet_client
    
    w3 = create_wallet_client(
        rpc_url="https://testnet-1.seismictest.net/rpc",
        private_key=private_key,
    )
    
    # Client's contract() method creates ShieldedContract
    contract = w3.seismic.contract(address=contract_address, abi=CONTRACT_ABI)
    
    # Now use any namespace
    tx_hash = contract.write.setNumber(42)
    DebugWriteResult
    AsyncShieldedContract
    PublicContract
    EncryptionState

    Yes

    DebugWriteResult
    hashtag
    Constructor

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    chain

    Chain

    Yes

    Chain configuration (e.g., seismicTestnet)

    transport

    Transport

    hashtag
    Return Type

    ShieldedPublicClient

    A viem PublicClient extended with ShieldedPublicActions, DepositContractPublicActions, and SRC20PublicActions.

    hashtag
    Usage

    hashtag
    Actions

    hashtag
    Shielded Public Actions

    Action
    Return Type
    Description

    getTeePublicKey()

    Promise<Hex>

    Fetch the TEE's secp256k1 public key via seismic_getTeePublicKey RPC

    getStorageAt()

    --

    Throws error (not supported on Seismic)

    hashtag
    Precompile Actions

    Action
    Return Type
    Description

    rng(params)

    Promise<bigint>

    Generate random numbers via the RNG precompile

    ecdh(params)

    Promise<Hex>

    ECDH key exchange via precompile

    hashtag
    SRC20 Actions

    Action
    Return Type
    Description

    watchSRC20EventsWithKey()

    Promise<() => void>

    Watch for SRC20 events with a decryption key

    See SRC20 Event Watching for the full flow, log types, and example usage.

    hashtag
    Deposit Contract Actions

    Action
    Return Type
    Description

    getDepositRoot()

    Promise<Hex>

    Query the deposit merkle root

    getDepositCount()

    Promise<Hex>

    Query the deposit count

    See Deposit Contract for parameters and validator registration via deposit().

    hashtag
    Standard viem Public Actions

    All standard viem PublicActions are available directly on the client:

    • getBlockNumber(), getBlock(), getTransaction()

    • getBalance(), getCode(), call()

    • estimateGas(), waitForTransactionReceipt()

    • All other viem public client methods

    hashtag
    getStorageAt Disabled

    circle-exclamation

    Seismic does not support eth_getStorageAt. Calling publicClient.getStorageAt() will throw an error. This is by design -- storage slots on Seismic may contain shielded data and cannot be read directly via RPC.

    hashtag
    Examples

    hashtag
    Fetching the TEE Public Key

    hashtag
    Using Precompiles

    hashtag
    Explorer URL Helpers

    Each helper accepts an optional tab argument to deep-link into a specific view. The helpers return string | null -- null when the chain has no configured block explorer.

    hashtag
    Tab values by item type

    Helper
    Tab type
    Values

    txExplorerUrl

    TxExplorerTab

    'index', 'token_transfers', 'internal', 'logs', 'state', 'raw_trace'

    addressExplorerUrl

    AddressExplorerTab

    'txs', 'token_transfers', 'tokens', 'internal_txns', 'coin_balance_history', 'logs', 'contract'

    hashtag
    Standalone functions

    The same helpers are exported as standalone functions that take an explicit chain and can be used without a client:

    hashtag
    See Also

    • Shielded Wallet Client -- Full-featured client with encryption and shielded writes

    • Deposit Contract -- Validator staking read/write actions

    • SRC20 Event Watching -- Watch and decrypt SRC20 token events

    • -- Chain configurations for Seismic networks

    • -- Precompile details and parameters

    • -- Encryption utilities and getEncryption()

    import { createShieldedPublicClient } from "seismic-viem";
    const publicClient = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    import { createShieldedPublicClient } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    
    const publicClient = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    
    // Standard viem public actions work as usual
    const blockNumber = await publicClient.getBlockNumber();
    const balance = await publicClient.getBalance({
      address: "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb",
    });
    import { createShieldedPublicClient } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    
    const publicClient = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    
    const teePublicKey = await publicClient.getTeePublicKey();
    console.log("TEE public key:", teePublicKey);
    import { createShieldedPublicClient } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    
    const publicClient = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    
    // Generate a random number
    const randomValue = await publicClient.rng({ numBytes: 1 });
    console.log("Random value:", randomValue);
    
    // ECDH key exchange
    const sharedSecret = await publicClient.ecdh({
      sk: "0x...",
      pk: "0x...",
    });
    
    // AES-GCM encryption
    const ciphertext = await publicClient.aesGcmEncryption({
      aesKey: "0x...",
      plaintext: "0x...",
      nonce: "0x...",
    });
    
    // HKDF key derivation
    const derivedKey = await publicClient.hdfk("0x...");
    const publicClient = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    
    const txUrl = publicClient.txExplorerUrl({ txHash: "0xabc123..." });
    const txLogs = publicClient.txExplorerUrl({
      txHash: "0xabc123...",
      tab: "logs",
    });
    const addrUrl = publicClient.addressExplorerUrl({
      address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
    });
    const blockUrl = publicClient.blockExplorerUrl({ blockNumber: 12345 });
    const tokenUrl = publicClient.tokenExplorerUrl({
      address: "0xYourTokenAddress",
      tab: "holders",
    });
    import {
      addressExplorerUrl,
      blockExplorerUrl,
      getExplorerUrl,
      tokenExplorerUrl,
      txExplorerUrl,
    } from "seismic-viem";
    
    const url = txExplorerUrl({
      chain: seismicTestnet,
      txHash: "0xabc...",
      tab: "logs",
    });
    
    // Low-level form used by all helpers internally:
    const raw = getExplorerUrl(seismicTestnet, {
      item: "address",
      id: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
      tab: "logs",
    });
    ,
    .twrite
    ,
    .tread
    ,
    .dwrite
    ) but with coroutine-based methods. All namespace methods return coroutines that must be awaited. Use this class in async/await applications for non-blocking contract interactions.

    hashtag
    Definition

    hashtag
    Constructor Parameters

    Parameter
    Type
    Required
    Description

    w3

    AsyncWeb3

    Yes

    Asynchronous AsyncWeb3 instance connected to Seismic RPC

    encryption

    hashtag
    Namespaces

    hashtag
    .write - Encrypted Write

    Sends encrypted transactions using TxSeismic (type 0x4a). Calldata is encrypted before broadcast.

    Returns: Coroutine[HexBytes] (transaction hash)

    Positional Arguments: *args - ABI function arguments (e.g. await contract.write.transfer(to, amount))

    Optional Parameters:

    • value: int - Wei to send (default: 0)

    • gas: int | None - Gas limit (default: 30_000_000 when omitted)

    • gas_price: int | None - Gas price in wei (default: network suggested)

    • security: [SeismicSecurityParams](../api-reference/transaction-types/seismic-security-params.md) | None - Security parameters for expiry

    hashtag
    .read - Encrypted Read

    Executes encrypted signed eth_call with encrypted calldata. Result is decrypted and ABI-decoded by the SDK. Single-output functions return the value directly (e.g. int, bool); multi-output functions return a tuple.

    Returns: Coroutine[Any] (ABI-decoded Python value)

    Positional Arguments: *args - ABI function arguments (e.g. await contract.read.balanceOf(owner))

    Optional Parameters:

    • value: int - Wei for call context (default: 0)

    • gas: int - Gas limit (default: 30_000_000)

    • security: [SeismicSecurityParams](../api-reference/transaction-types/seismic-security-params.md) | None - Security parameters for expiry

    hashtag
    .twrite - Transparent Write

    Sends standard async eth_sendTransaction with unencrypted calldata.

    Returns: Coroutine[HexBytes] (transaction hash)

    Positional Arguments: *args - ABI function arguments

    Optional Parameters:

    • value: int - Wei to send (default: 0)

    • **tx_params: Any - Additional transaction parameters (gas, gasPrice, etc.)

    hashtag
    .tread - Transparent Read

    Executes standard async eth_call with unencrypted calldata. Result is ABI-decoded by the SDK. Single-output functions return the value directly; multi-output functions return a tuple.

    Returns: Coroutine[Any] (ABI-decoded Python value)

    Positional Arguments: *args - ABI function arguments

    hashtag
    .dwrite - Debug Write

    Like .write but returns debug information including plaintext and encrypted views. Transaction is actually broadcast.

    Returns: Coroutine[DebugWriteResult] (DebugWriteResult)

    Positional Arguments: *args - ABI function arguments

    Optional Parameters: Same as .write

    hashtag
    Examples

    hashtag
    Basic Encrypted Write

    hashtag
    Encrypted Read

    hashtag
    Transparent Operations

    hashtag
    Debug Write

    hashtag
    Concurrent Operations

    hashtag
    With Transaction Parameters

    hashtag
    Batch Processing

    hashtag
    Using EIP-712 Signing

    hashtag
    Instantiation via Async Client

    hashtag
    Error Handling

    hashtag
    Context Manager Pattern

    hashtag
    Notes

    • All methods return coroutines: Must use await with every namespace call

    • Concurrent operations: Use asyncio.gather() for parallel reads/writes

    • Dynamic method access: Contract methods resolved via __getattr__ at runtime

    • ABI remapping: Shielded types automatically remapped (same as sync version)

    • Connection pooling: AsyncWeb3 can reuse connections for better performance

    • Gas defaults: .write uses 30_000_000 when gas is omitted; .twrite follows normal web3.py/provider transaction behavior

    • EIP-712 vs raw: Same signing options as sync version

    • Error handling: Use try/except around await calls for RPC errors

    hashtag
    See Also

    • ShieldedContract - Synchronous version of this class

    • AsyncPublicContract - Async read-only contract wrapper

    • create_async_wallet_client - Create async client

    • - Manages encryption keys

    • - Debug write return type

    • - Transaction expiry parameters

    • - Detailed namespace documentation

    class AsyncShieldedContract:
        def __init__(
            self,
            w3: AsyncWeb3,
            encryption: EncryptionState,
            private_key: PrivateKey,
            address: ChecksumAddress,
            abi: list[dict[str, Any]],
            eip712: bool = False,
        ) -> None:
            ...
    import asyncio
    from seismic_web3 import create_async_wallet_client, AsyncShieldedContract
    
    async def main():
        w3 = await create_async_wallet_client(
            provider_url="https://testnet-1.seismictest.net/rpc",
            private_key=private_key,
        )
    
        contract = AsyncShieldedContract(
            w3=w3,
            encryption=w3.seismic.encryption,
            private_key=private_key,
            address="0x1234567890123456789012345678901234567890",
            abi=CONTRACT_ABI,
        )
    
        # Encrypted write - must await
        tx_hash = await contract.write.setNumber(42)
        print(f"Transaction: {tx_hash.to_0x_hex()}")
    
        # Wait for confirmation
        receipt = await w3.eth.wait_for_transaction_receipt(tx_hash)
        print(f"Status: {receipt['status']}")
    
    asyncio.run(main())
    async def read_example(contract: AsyncShieldedContract):
        # Encrypted read — auto-decoded, must await
        number = await contract.read.getNumber()  # int
        print(f"Number: {number}")
    async def transparent_example(contract: AsyncShieldedContract, w3: AsyncWeb3):
        # Transparent write - calldata visible on-chain
        tx_hash = await contract.twrite.setNumber(42)
        receipt = await w3.eth.wait_for_transaction_receipt(tx_hash)
        print(f"Status: {receipt['status']}")
    
        # Transparent read — standard eth_call, auto-decoded
        number = await contract.tread.getNumber()
        print(f"Result: {number}")
    async def debug_example(contract: AsyncShieldedContract, w3: AsyncWeb3):
        # Debug write - returns plaintext and encrypted views
        debug_result = await contract.dwrite.transfer("0xRecipient...", 1000)
    
        print(f"Transaction hash: {debug_result.tx_hash.to_0x_hex()}")
        print(f"Plaintext data: {debug_result.plaintext_tx.data.to_0x_hex()}")
        print(f"Encrypted data: {debug_result.shielded_tx.data.to_0x_hex()}")
    
        # Transaction is actually broadcast
        receipt = await w3.eth.wait_for_transaction_receipt(debug_result.tx_hash)
        print(f"Confirmed in block: {receipt['blockNumber']}")
    async def concurrent_example(contract: AsyncShieldedContract):
        # Execute multiple reads concurrently — each is auto-decoded
        balances = await asyncio.gather(
            contract.tread.balanceOf("0xAddress1..."),
            contract.tread.balanceOf("0xAddress2..."),
            contract.tread.balanceOf("0xAddress3..."),
        )
    
        for i, balance in enumerate(balances):
            print(f"Balance {i}: {balance}")
    async def advanced_write(contract: AsyncShieldedContract):
        # Custom gas and value
        tx_hash = await contract.write.deposit(
            value=10**18,  # 1 ETH
            gas=200_000,
            gas_price=20 * 10**9,  # 20 gwei
        )
    
        # With security parameters
        from seismic_web3.transaction_types import SeismicSecurityParams
    
        security = SeismicSecurityParams(blocks_window=100)
        tx_hash = await contract.write.withdraw(
            amount,
            security=security,
        )
    async def batch_writes(contract: AsyncShieldedContract, recipients: list[str]):
        # Send multiple transactions concurrently
        tx_hashes = await asyncio.gather(
            *[contract.write.transfer(recipient, 100) for recipient in recipients]
        )
    
        print(f"Sent {len(tx_hashes)} transactions")
    
        # Wait for all confirmations
        receipts = await asyncio.gather(
            *[w3.eth.wait_for_transaction_receipt(tx_hash) for tx_hash in tx_hashes]
        )
    
        successful = sum(1 for r in receipts if r['status'] == 1)
        print(f"{successful}/{len(receipts)} successful")
    async def eip712_example():
        w3 = await create_async_wallet_client(...)
    
        # Enable EIP-712 for typed data signing
        contract = AsyncShieldedContract(
            w3=w3,
            encryption=w3.seismic.encryption,
            private_key=private_key,
            address=contract_address,
            abi=CONTRACT_ABI,
            eip712=True,  # Use EIP-712 instead of raw signing
        )
    
        tx_hash = await contract.write.setNumber(123)
    async def client_pattern():
        # Most common pattern - let the client create the contract
        w3 = await create_async_wallet_client(
            provider_url="https://testnet-1.seismictest.net/rpc",
            private_key=private_key,
        )
    
        # Client's contract() method creates AsyncShieldedContract
        contract = w3.seismic.contract(address=contract_address, abi=CONTRACT_ABI)
    
        # Now use any namespace (must await)
        tx_hash = await contract.write.setNumber(42)
    async def error_handling(contract: AsyncShieldedContract):
        try:
            tx_hash = await contract.write.withdraw(123)
            receipt = await w3.eth.wait_for_transaction_receipt(tx_hash)
    
            if receipt['status'] != 1:
                print("Transaction failed on-chain")
        except ValueError as e:
            print(f"RPC error: {e}")
        except Exception as e:
            print(f"Unexpected error: {e}")
    async def context_pattern():
        async with create_async_wallet_client(...) as w3:
            contract = AsyncShieldedContract(...)
    
            tx_hash = await contract.write.setNumber(42)
            receipt = await w3.eth.wait_for_transaction_receipt(tx_hash)
        # Connection automatically closed

    create_async_public_client

    Create async Web3 instance with public (read-only) Seismic access

    Create an asynchronous AsyncWeb3 instance with public (read-only) Seismic access.

    hashtag
    Overview

    create_async_public_client() creates an async client for read-only operations on the Seismic network. No private key is required. The w3.seismic namespace provides only public read operations: get_tee_public_key(), get_deposit_root(), get_deposit_count(), and contract() (with .tread only).

    Supports both HTTP and WebSocket connections for efficient async queries and real-time monitoring.

    hashtag
    Signature

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns

    Type
    Description

    hashtag
    Examples

    hashtag
    Basic Usage (HTTP)

    hashtag
    WebSocket Connection

    hashtag
    Using Chain Configuration

    hashtag
    Async Application

    hashtag
    Read-Only Contract Access

    hashtag
    Monitoring Pattern

    hashtag
    Parallel Queries

    hashtag
    Context Manager Pattern

    hashtag
    How It Works

    The function performs three steps:

    1. Create provider

    2. Create AsyncWeb3 instance

    3. Attach public Seismic namespace

    No TEE public key fetching or encryption setup is performed since the client cannot perform shielded operations.

    hashtag
    Client Capabilities

    hashtag
    Standard AsyncWeb3 Methods (e.g. w3.eth, w3.net)

    • await get_block(), await get_transaction(), await get_balance()

    • await call(), await estimate_gas()

    hashtag
    Public Seismic Methods (w3.seismic)

    • - Get TEE public key

    • - Query deposit merkle root

    • - Query deposit count

    hashtag
    NOT Available

    • - Requires private key

    • - Requires private key

    • - Requires private key and encryption

    hashtag
    HTTP vs WebSocket

    hashtag
    Notes

    • The function is synchronous (no await needed) but returns an AsyncWeb3 instance whose methods are async

    • No private key required or accepted

    • No encryption setup performed

    hashtag
    Use Cases

    • Async block explorers and chain analytics

    • Real-time monitoring dashboards with WebSocket subscriptions

    • High-throughput read-only services

    • Async data aggregation pipelines

    hashtag
    Warnings

    • Connection cleanup - Close WebSocket connections properly to avoid resource leaks

    • Error handling - WebSocket connections can drop; implement reconnection logic for production

    • HTTPS/WSS recommended - Use secure protocols in production to prevent MITM attacks

    hashtag
    See Also

    • - Sync variant (HTTP only)

    • - Async client with private key

    • - The async public w3.seismic namespace

    create_async_wallet_client

    Create async Web3 instance with full Seismic wallet capabilities

    Create an asynchronous AsyncWeb3 instance with full Seismic wallet capabilities.

    hashtag
    Overview

    create_async_wallet_client() is the async factory function for creating a client that can perform shielded writes, signed reads, and deposits. It supports both HTTP and WebSocket connections, fetches the TEE public key asynchronously, derives encryption state via ECDHarrow-up-right, and attaches a fully-configured w3.seismic namespace.

    The returned client works with all standard async web3.py APIs (await w3.eth.get_block(), etc.) plus the additional w3.seismic namespace for Seismic-specific operations.

    hashtag
    Signature

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns

    Type
    Description

    hashtag
    Examples

    hashtag
    Basic Usage (HTTP)

    hashtag
    WebSocket Connection

    hashtag
    Using Chain Configuration

    hashtag
    Context Manager Pattern

    hashtag
    Async Application

    hashtag
    With Custom Encryption Key

    hashtag
    How It Works

    The function performs six steps:

    1. Create provider

    2. Create AsyncWeb3 instance

    3. Fetch TEE public key (async RPC call)

    4. Generate encryption keypair

    hashtag
    Client Capabilities

    The returned client provides:

    hashtag
    Standard AsyncWeb3 Methods (e.g. w3.eth, w3.net)

    • await get_block(), await get_transaction(), await get_balance()

    • await send_raw_transaction(), await wait_for_transaction_receipt()

    hashtag
    Async Seismic Methods (w3.seismic)

    • - Send shielded transactions

    • - Debug shielded transactions

    • - Execute signed reads

    hashtag
    HTTP vs WebSocket

    hashtag
    Encryption

    The client automatically:

    • Fetches the network's TEE public key asynchronously

    • Performs ECDH key exchange using encryption_sk (or generates a random one)

    • Derives a shared AES-GCM key via HKDF

    Access the encryption state at w3.seismic.encryption if needed for advanced use cases.

    hashtag
    Notes

    • The function is async and must be await-ed

    • Makes one asynchronous RPC call to fetch the TEE public key

    • If encryption_sk is None

    hashtag
    Warnings

    • Private key security - Never log or expose private keys. Use environment variables or secure key management

    • Connection cleanup - Close WebSocket connections properly to avoid resource leaks

    • Error handling - WebSocket connections can drop; implement reconnection logic for production

    hashtag
    See Also

    • - Sync variant (HTTP only)

    • - Async read-only client

    • - Encryption state class

    EncryptionState

    Holds AES key and encryption keypair derived from ECDH

    Holds the -GCM key and encryption keypair derived from key exchange.

    hashtag
    Overview

    EncryptionState encapsulates all cryptographic material needed for shielded transactions and signed reads. It's created by during wallet client setup and attached to .encryption.

    Yes

    viem transport (e.g., http())

    publicRequest()

    Promise<any>

    Raw RPC request to the node

    explorerUrl(opts)

    string | null

    Generate a block explorer URL

    addressExplorerUrl(address)

    string | null

    Explorer URL for an address

    blockExplorerUrl(block)

    string | null

    Explorer URL for a block

    txExplorerUrl(hash)

    string | null

    Explorer URL for a transaction

    tokenExplorerUrl(address)

    string | null

    Explorer URL for a token

    aesGcmEncryption(params)

    Promise<Hex>

    AES-GCM encrypt via precompile

    aesGcmDecryption(params)

    Promise<string>

    AES-GCM decrypt via precompile

    hdfk(ikm)

    Promise<Hex>

    HKDF key derivation via precompile

    secp256k1Signature(params)

    Promise<Signature>

    secp256k1 signing via precompile

    tokenExplorerUrl

    TokenExplorerTab

    'token_transfers', 'holders', 'contract'

    blockExplorerUrl

    BlockExplorerTab

    'index', 'txs'

    Chains
    Precompiles
    Encryption
    PrivateKey

    Yes

    Encryption state for shielded operations

    private_key

    PrivateKey

    Yes

    32-byte secp256k1 private key for signing transactions

    address

    ChecksumAddress

    Yes

    Contract address (checksummed Ethereum address)

    abi

    list[dict[str, Any]]

    Yes

    Contract ABI (list of function entries)

    eip712

    bool

    No

    Use EIP-712 typed data signing (default: False)

    EncryptionState
    DebugWriteResult
    SeismicSecurityParams
    Contract Namespaces
    EncryptionState

    If True, uses WebSocketProvider (persistent connection, supports subscriptions). Otherwise uses AsyncHTTPProvider. Default: False. WebSocket is only available on async clients — sync clients are HTTP-only

    All other standard read-only async web3.py functionality
    - Create contract wrappers (
    .tread
    methods are async)
    - Requires private key
  • Contract .swrite and .sread methods - Require private key

  • Not supported

    Supported (eth.subscribe)

    Resource usage

    Lower idle usage

    Keeps connection open

    Use case

    One-off queries

    Real-time monitoring, subscriptions

    No RPC calls during client creation (lightweight)

  • Cannot perform any write operations or shielded reads

  • Contract wrappers only expose .tread (transparent read, async)

  • All w3.seismic methods are async and must be await-ed

  • WebSocket connections should be properly closed when done

  • For write operations, use create_async_wallet_client()

  • For sync operations, use create_public_client()

  • Event monitoring and alerting systems

  • Price oracles with low-latency requirements

  • Chains Configuration - Pre-configured chain constants
  • Contract Instances - Working with contract wrappers

  • provider_url

    str

    Yes

    HTTP(S) or WS(S) URL of the Seismic node

    ws

    bool

    AsyncWeb3

    An AsyncWeb3 instance with w3.seismic namespace attached (AsyncSeismicPublicNamespace)

    Aspect

    AsyncHTTPProvider (ws=False)

    WebSocketProvider (ws=True)

    Connection

    New connection per request

    Persistent connection

    Latency

    Higher per-request overhead

    Lower latency

    await get_tee_public_key()
    await get_deposit_root()
    await get_deposit_count()
    contract()
    send_shielded_transaction()
    debug_send_shielded_transaction()
    signed_call()
    create_public_client
    create_async_wallet_client
    AsyncSeismicPublicNamespace

    No

    Subscriptions

    deposit()
    def create_async_public_client(
        provider_url: str,
        *,
        ws: bool = False,
    ) -> AsyncWeb3
    from seismic_web3 import create_async_public_client
    
    # Create async public client
    w3 = create_async_public_client("https://testnet-1.seismictest.net/rpc")
    
    # Query TEE public key
    tee_pk = await w3.seismic.get_tee_public_key()
    print(f"TEE public key: {tee_pk.to_0x_hex()}")
    
    # Query deposit info
    root = await w3.seismic.get_deposit_root()
    count = await w3.seismic.get_deposit_count()
    print(f"Deposit root: {root.to_0x_hex()}, count: {count}")
    from seismic_web3 import create_async_public_client
    
    # WebSocket provider for persistent connection
    w3 = create_async_public_client(
        "wss://testnet-1.seismictest.net/ws",
        ws=True,
    )
    
    # Subscribe to new blocks
    async for block in w3.eth.subscribe("newHeads"):
        print(f"New block: {block['number']}")
    
        # Query deposit count at each new block
        count = await w3.seismic.get_deposit_count()
        print(f"Current deposit count: {count}")
    from seismic_web3 import SEISMIC_TESTNET
    
    # Recommended: use chain config with HTTP
    w3 = SEISMIC_TESTNET.async_public_client()
    
    # Or with WebSocket (uses ws_url from chain config)
    w3 = SEISMIC_TESTNET.async_public_client(ws=True)
    import asyncio
    from seismic_web3 import create_async_public_client
    
    async def main():
        w3 = create_async_public_client("https://testnet-1.seismictest.net/rpc")
    
        # Get current block
        block = await w3.eth.get_block("latest")
        print(f"Latest block: {block['number']}")
    
        # Get balance
        address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
        balance = await w3.eth.get_balance(address)
        print(f"Balance: {w3.from_wei(balance, 'ether')} ETH")
    
        # Query deposit info
        deposit_count = await w3.seismic.get_deposit_count()
        print(f"Total deposits: {deposit_count}")
    
    asyncio.run(main())
    from seismic_web3 import create_async_public_client
    
    async def query_contract():
        w3 = create_async_public_client("https://testnet-1.seismictest.net/rpc")
    
        # Create contract wrapper (read-only)
        contract = w3.seismic.contract(
            address="0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
            abi=contract_abi,
        )
    
        # Only transparent reads are available
        result = await contract.tread.balanceOf("0x1234...")
        print(f"Balance: {result}")
    from seismic_web3 import create_async_public_client
    import asyncio
    
    async def monitor_deposits():
        w3 = create_async_public_client("wss://testnet-1.seismictest.net/ws", ws=True)
    
        last_count = await w3.seismic.get_deposit_count()
        print(f"Starting deposit count: {last_count}")
    
        async for block in w3.eth.subscribe("newHeads"):
            current_count = await w3.seismic.get_deposit_count()
    
            if current_count > last_count:
                print(f"New deposits detected! Count: {current_count}")
                print(f"Block: {block['number']}")
                last_count = current_count
    
    asyncio.run(monitor_deposits())
    from seismic_web3 import create_async_public_client
    import asyncio
    
    async def get_chain_stats():
        w3 = create_async_public_client("https://testnet-1.seismictest.net/rpc")
    
        # Run multiple queries in parallel
        block, tee_pk, deposit_root, deposit_count = await asyncio.gather(
            w3.eth.get_block("latest"),
            w3.seismic.get_tee_public_key(),
            w3.seismic.get_deposit_root(),
            w3.seismic.get_deposit_count(),
        )
    
        return {
            "block_number": block["number"],
            "tee_public_key": tee_pk.to_0x_hex(),
            "deposit_root": deposit_root.to_0x_hex(),
            "deposit_count": deposit_count,
        }
    from seismic_web3 import create_async_public_client
    
    async with create_async_public_client(
        "wss://testnet-1.seismictest.net/ws",
        ws=True,
    ) as w3:
        # WebSocket connection will be properly closed
        block = await w3.eth.get_block("latest")
        print(f"Block: {block['number']}")
    if ws:
        provider = WebSocketProvider(provider_url)
    else:
        provider = AsyncHTTPProvider(provider_url)
    w3 = AsyncWeb3(provider)
    w3.seismic = AsyncSeismicPublicNamespace(w3)

    32-byte secp256k1 private key for signing transactions

    encryption_sk

    No

    Optional 32-byte key for ECDH. If None, a random ephemeral key is generated

    ws

    bool

    No

    If True, uses WebSocketProvider (persistent connection, supports subscriptions). Otherwise uses AsyncHTTPProvider. Default: False. WebSocket is only available on async clients — sync clients are HTTP-only

    (if
    encryption_sk
    is
    None
    , a random ephemeral key is created)
  • Derive encryption state (ECDH + HKDFarrow-up-right)

    encryption = get_encryption(network_pk, encryption_sk)
  • Attach Seismic namespace

    w3.seismic = AsyncSeismicNamespace(w3, encryption, private_key)
  • All other standard async web3.py functionality
    - Deposit ETH/tokens
  • await get_tee_public_key() - Get TEE public key

  • await get_deposit_root() - Query deposit merkle root

  • await get_deposit_count() - Query deposit count

  • contract() - Create contract wrappers (methods are async)

  • Not supported

    Supported (eth.subscribe)

    Resource usage

    Lower idle usage

    Keeps connection open

    Use case

    One-off transactions

    Real-time monitoring, subscriptions

    Uses this key to encrypt all shielded transaction calldata and signed reads
    , a random ephemeral key is generated
  • The encryption key is separate from the transaction signing key

  • WebSocket connections should be properly closed when done

  • All w3.seismic methods are async and must be await-ed

  • For sync operations, use create_wallet_client()

  • HTTPS/WSS recommended - Use secure protocols in production to prevent MITM attacks
    - Encryption derivation function
  • AsyncSeismicNamespace - The async w3.seismic namespace

  • Chains Configuration - Pre-configured chain constants

  • async def create_async_wallet_client(
        provider_url: str,
        private_key: PrivateKey,
        *,
        encryption_sk: PrivateKey | None = None,
        ws: bool = False,
    ) -> AsyncWeb3

    provider_url

    str

    Yes

    HTTP(S) or WS(S) URL of the Seismic node

    private_key

    PrivateKey

    AsyncWeb3

    An AsyncWeb3 instance with w3.seismic namespace attached (AsyncSeismicNamespace)

    import os
    from seismic_web3 import create_async_wallet_client, PrivateKey
    
    # Load private key
    private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
    # Create async wallet client
    w3 = await create_async_wallet_client(
        "https://testnet-1.seismictest.net/rpc",
        private_key=private_key,
    )
    
    # Now use w3.seismic for Seismic operations
    contract = w3.seismic.contract(address, abi)
    tx_hash = await contract.swrite.transfer(recipient, 1000)
    receipt = await w3.eth.wait_for_transaction_receipt(tx_hash)
    import os
    from seismic_web3 import create_async_wallet_client, PrivateKey
    
    private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
    # WebSocket provider for persistent connection
    w3 = await create_async_wallet_client(
        "wss://testnet-1.seismictest.net/ws",
        private_key=private_key,
        ws=True,
    )
    
    # Subscribe to new blocks
    async for block in w3.eth.subscribe("newHeads"):
        print(f"New block: {block['number']}")
    import os
    from seismic_web3 import SEISMIC_TESTNET, PrivateKey
    
    private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
    # Recommended: use chain config with HTTP
    w3 = await SEISMIC_TESTNET.async_wallet_client(private_key)
    
    # Or with WebSocket (uses ws_url from chain config)
    w3 = await SEISMIC_TESTNET.async_wallet_client(private_key, ws=True)
    import os
    from seismic_web3 import create_async_wallet_client, PrivateKey
    
    private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
    # Use context manager to ensure cleanup
    async with create_async_wallet_client(
        "wss://testnet-1.seismictest.net/ws",
        private_key=private_key,
        ws=True,
    ) as w3:
        contract = w3.seismic.contract(address, abi)
        tx_hash = await contract.swrite.transfer(recipient, 1000)
        receipt = await w3.eth.wait_for_transaction_receipt(tx_hash)
    import asyncio
    import os
    from seismic_web3 import create_async_wallet_client, PrivateKey
    
    async def main():
        private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    
        w3 = await create_async_wallet_client(
            "https://testnet-1.seismictest.net/rpc",
            private_key=private_key,
        )
    
        # Get current block
        block = await w3.eth.get_block("latest")
        print(f"Latest block: {block['number']}")
    
        # Get balance
        address = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"
        balance = await w3.eth.get_balance(address)
        print(f"Balance: {w3.from_wei(balance, 'ether')} ETH")
    
    asyncio.run(main())
    import os
    from seismic_web3 import create_async_wallet_client, PrivateKey
    
    async def setup_client():
        signing_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
        encryption_key = PrivateKey(os.urandom(32))  # Custom encryption keypair
    
        w3 = await create_async_wallet_client(
            "https://testnet-1.seismictest.net/rpc",
            private_key=signing_key,
            encryption_sk=encryption_key,
        )
    
        return w3
    if ws:
        provider = WebSocketProvider(provider_url)
    else:
        provider = AsyncHTTPProvider(provider_url)
    w3 = AsyncWeb3(provider)
    network_pk = await async_get_tee_public_key(w3)

    Aspect

    AsyncHTTPProvider (ws=False)

    WebSocketProvider (ws=True)

    Connection

    New connection per request

    Persistent connection

    Latency

    Higher per-request overhead

    Lower latency

    await send_shielded_transaction()
    await debug_send_shielded_transaction()
    await signed_call()
    create_wallet_client
    create_async_public_client
    EncryptionState

    Yes

    encryption_sk = encryption_sk or PrivateKey(os.urandom(32))

    Subscriptions

    await deposit()
    get_encryption
    The class provides encrypt() and decrypt() methods that handle AES-GCM encryption with metadata-bound Additional Authenticated Data (AAD).

    hashtag
    Definition

    hashtag
    Attributes

    Attribute
    Type
    Description

    aes_key

    32-byte AES-256 key derived from ECDH +

    encryption_pubkey

    Client's 33-byte compressed secp256k1 public key

    hashtag
    Methods

    hashtag
    encrypt()

    Encrypt plaintext calldata with metadata-bound AAD.

    hashtag
    Signature

    hashtag
    Parameters

    Parameter
    Type
    Description

    plaintext

    HexBytes

    Raw calldata to encrypt

    nonce

    12-byte AES-GCM nonce

    hashtag
    Returns

    Type
    Description

    HexBytes

    Ciphertext with 16-byte authentication tag appended

    hashtag
    Example

    hashtag
    decrypt()

    Decrypt ciphertext with metadata-bound AAD.

    hashtag
    Signature

    hashtag
    Parameters

    Parameter
    Type
    Description

    ciphertext

    HexBytes

    Encrypted data (includes 16-byte auth tag)

    nonce

    12-byte AES-GCM nonce

    hashtag
    Returns

    Type
    Description

    HexBytes

    Decrypted plaintext

    hashtag
    Raises

    • cryptography.exceptions.InvalidTag - If authentication fails (wrong key, tampered data, or mismatched metadata)

    hashtag
    Example

    hashtag
    Examples

    hashtag
    Access from Client

    hashtag
    Manual Encryption Workflow

    hashtag
    Custom Encryption Key

    hashtag
    Verify Encryption/Decryption

    hashtag
    How It Works

    hashtag
    Initialization

    When created, EncryptionState automatically initializes an internal AesGcmCrypto instance:

    hashtag
    Encryption

    1. Encode metadata as AAD using encode_metadata_as_aad()

    2. Call AesGcmCrypto.encrypt(plaintext, nonce, aad)

    3. Return ciphertext with 16-byte authentication tag

    hashtag
    Decryption

    1. Encode metadata as AAD

    2. Call AesGcmCrypto.decrypt(ciphertext, nonce, aad)

    3. Verify authentication tag (raises InvalidTag if fails)

    4. Return plaintext

    hashtag
    AAD Binding

    The Additional Authenticated Data (AAD) ensures that ciphertext is cryptographically bound to transaction metadata:

    • message_version

    • chain_id

    • client_pubkey

    • nonce_seed

    • recent_block_hash

    • expires_at_block

    If any metadata field changes, decryption will fail even with the correct key and nonce.

    hashtag
    Notes

    • Pure computation - no I/O operations

    • Works in both sync and async contexts

    • Created automatically by create_wallet_client() and create_async_wallet_client()

    • You rarely need to call or directly - the SDK handles this

    • The internal _crypto field is excluded from repr() and comparison

    • Authentication tag is always 16 bytes (AES-GCM standard)

    hashtag
    Security Considerations

    • Key derivation - AESarrow-up-right key is derived from ECDHarrow-up-right + HKDFarrow-up-right, ensuring forward secrecy

    • AAD binding - Metadata binding prevents ciphertext reuse or manipulation

    • Nonce uniqueness - Nonces must be unique per encryption; SDK generates fresh nonces automatically

    • Key storage - encryption_private_key should be stored securely if deterministic keys are used

    hashtag
    See Also

    • get_encryption - Derive encryption state from TEE public key

    • create_wallet_client - Sync client factory (creates EncryptionState)

    • create_async_wallet_client - Async client factory

    • - 12-byte nonce type

    • - Metadata structure

    • - How shielded transactions work

    AESarrow-up-right
    ECDHarrow-up-right
    get_encryption()
    w3.seismic
    @dataclass
    class EncryptionState:
        """Holds the AES key and encryption keypair derived from ECDH.
    
        Created by :func:`get_encryption` during client setup.  Pure
        computation - works in both sync and async contexts.
    
        Attributes:
            aes_key: 32-byte AES-256 key derived from ECDH + HKDF.
            encryption_pubkey: Client's compressed secp256k1 public key.
            encryption_private_key: Client's secp256k1 private key.
        """
    
        aes_key: Bytes32
        encryption_pubkey: CompressedPublicKey
        encryption_private_key: PrivateKey
    def encrypt(
        self,
        plaintext: HexBytes,
        nonce: EncryptionNonce,
        metadata: TxSeismicMetadata,
    ) -> HexBytes
    from seismic_web3 import get_encryption, EncryptionNonce
    from hexbytes import HexBytes
    import os
    
    # Setup encryption state
    encryption = get_encryption(tee_public_key, client_private_key)
    
    # Encrypt calldata
    plaintext = HexBytes("0x1234abcd...")
    nonce = EncryptionNonce(os.urandom(12))
    
    ciphertext = encryption.encrypt(
        plaintext=plaintext,
        nonce=nonce,
        metadata=tx_metadata,
    )
    
    # Ciphertext is len(plaintext) + 16 bytes (auth tag)
    assert len(ciphertext) == len(plaintext) + 16
    def decrypt(
        self,
        ciphertext: HexBytes,
        nonce: EncryptionNonce,
        metadata: TxSeismicMetadata,
    ) -> HexBytes
    from seismic_web3 import get_encryption
    from cryptography.exceptions import InvalidTag
    
    encryption = get_encryption(tee_public_key, client_private_key)
    
    try:
        plaintext = encryption.decrypt(
            ciphertext=encrypted_data,
            nonce=nonce,
            metadata=tx_metadata,
        )
        print(f"Decrypted: {plaintext.to_0x_hex()}")
    except InvalidTag:
        print("Decryption failed: authentication tag mismatch")
    import os
    from seismic_web3 import create_wallet_client, PrivateKey
    
    private_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    w3 = create_wallet_client("https://testnet-1.seismictest.net/rpc", private_key=private_key)
    
    # Access encryption state
    encryption = w3.seismic.encryption
    
    print(f"AES key: {encryption.aes_key.to_0x_hex()}")
    print(f"Client pubkey: {encryption.encryption_pubkey.to_0x_hex()}")
    import os
    from seismic_web3 import get_encryption, PrivateKey, CompressedPublicKey
    from hexbytes import HexBytes
    
    # Get TEE public key from node
    tee_pk = CompressedPublicKey("0x02abcd...")
    
    # Create encryption state
    client_sk = PrivateKey(os.urandom(32))
    encryption = get_encryption(tee_pk, client_sk)
    
    # Build transaction metadata (see TxSeismicMetadata docs)
    metadata = ...  # TxSeismicMetadata for the transaction being encrypted
    
    # Encrypt some data
    plaintext = HexBytes("0x1234abcd")
    nonce = os.urandom(12)
    
    ciphertext = encryption.encrypt(
        plaintext=plaintext,
        nonce=nonce,
        metadata=metadata,
    )
    
    # Decrypt it back
    decrypted = encryption.decrypt(
        ciphertext=ciphertext,
        nonce=nonce,
        metadata=metadata,
    )
    
    assert decrypted == plaintext
    import os
    from seismic_web3 import get_encryption, PrivateKey, CompressedPublicKey
    
    # Use a deterministic key (e.g., derived from mnemonic)
    client_sk = PrivateKey.from_hex_str(os.environ["CLIENT_KEY"])
    
    # Or use a random ephemeral key
    # client_sk = PrivateKey(os.urandom(32))
    
    tee_pk = CompressedPublicKey("0x02abcd...")
    encryption = get_encryption(tee_pk, client_sk)
    
    # Store client_sk securely if you need to recreate the same encryption state later
    from seismic_web3 import get_encryption, PrivateKey, CompressedPublicKey
    from cryptography.exceptions import InvalidTag
    import os
    
    encryption = get_encryption(tee_pk, client_sk)
    
    plaintext = b"Hello, Seismic!"
    nonce = os.urandom(12)
    
    # Encrypt
    ciphertext = encryption.encrypt(plaintext, nonce, metadata)
    
    # Decrypt with correct parameters
    assert encryption.decrypt(ciphertext, nonce, metadata) == plaintext
    
    # Decrypt with wrong nonce - should fail
    wrong_nonce = os.urandom(12)
    try:
        encryption.decrypt(ciphertext, wrong_nonce, metadata)
        assert False, "Should have raised InvalidTag"
    except InvalidTag:
        print("Authentication failed as expected")
    def __post_init__(self) -> None:
        self._crypto = AesGcmCrypto(self.aes_key)

    Shielded Writes

    Send encrypted write transactions with shieldedWriteContract

    Shielded writes encrypt transaction calldata before submission. This prevents calldata from being visible on-chain -- an observer can see that a transaction was sent to a particular contract address, but not what function was called or what arguments were passed.

    seismic-viem provides several approaches:

    • contract.write.functionName() -- smart routing via getShieldedContract. Auto-detects shielded parameters and encrypts only when needed.

    • contract.swrite.functionName() -- force shielded via . Always encrypts, regardless of parameter types.

    • shieldedWriteContract() -- standalone function, same API shape as viem's writeContract. Always encrypts.

    • walletClient.writeContract() -- smart routing via the wallet client. Same auto-detection as contract.write.

    • walletClient.swriteContract() -- force shielded via the wallet client. Always encrypts.

    The shielded paths all produce the same on-chain result: an encrypted type 0x4A Seismic transaction.

    hashtag
    Standalone: shieldedWriteContract

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns

    Promise<Hash> -- the transaction hash.

    hashtag
    Example


    hashtag
    Send + Inspect: shieldedWriteContractDebug

    Broadcasts a shielded transaction (like shieldedWriteContract) and additionally returns the plaintext transaction view and the shielded (encrypted) transaction view alongside the resulting transaction hash. Useful for inspecting exactly what the SDK encrypted and submitted.

    circle-info

    Despite the "debug" flavor of the name, shieldedWriteContractDebug does send the transaction -- the returned txHash is a real on-chain hash, not a dry run.


    hashtag
    Low-level: sendShieldedTransaction

    For cases where you have raw transaction data instead of ABI-encoded calls -- for example, contract deployments, pre-encoded calldata, or bypassing the contract abstraction entirely:


    hashtag
    How It Works

    When you call shieldedWriteContract (or contract.swrite.functionName), the SDK performs the following steps:

    1. ABI-encode the function call into plaintext calldata

    2. Build Seismic metadata -- encryption nonce, recent block hash, expiry block

    3. Encrypt calldata with AES-GCM using a shared key derived via ECDH between your ephemeral keypair and the node's TEE public key

    The encrypted calldata is bound to the transaction context (chain ID, nonce, block hash, expiry) via AES-GCM additional authenticated data, so it cannot be replayed or tampered with.


    hashtag
    Security Parameters

    Every shielded transaction includes a block-hash freshness check and an expiry window. The defaults are sensible for most cases, but you can override them per-call via SeismicSecurityParams, passed as the optional third argument to shieldedWriteContract, shieldedWriteContractDebug, sendShieldedTransaction, signedReadContract, and signedCall:

    Parameter
    Type
    Default
    Description
    circle-info

    securityParams only applies to the low-level shielded paths (shieldedWriteContract, shieldedWriteContractDebug, sendShieldedTransaction, signedReadContract, signedCall, and the .swrite/.sread/.dwrite

    circle-info

    The default 100-block window, random nonce, and latest block hash are appropriate for nearly all use cases. Override these only if you have a specific reason -- for example, reducing the window for time-sensitive operations or pinning the block hash in tests.

    hashtag
    See Also

    • -- getShieldedContract with .write and .dwrite namespaces

    • -- Authenticated reads that also use the encryption pipeline

    • -- ECDH key exchange and AES-GCM details

    get_encryption

    Derive encryption state from TEE public key using ECDH

    Derive encryption state from a TEE public key using ECDH key exchange.

    hashtag
    Overview

    get_encryption() performs ECDH key exchange between a client private key and the TEE's public key to derive a shared AES-GCM key. It returns an object containing the AES key, client public key, and client private key.

    This is a pure computation function with no I/O - it works in both sync and async contexts.

    SRC20 Event Watching

    Watch and decrypt SRC20 token events

    SRC20 is Seismic's confidential token standard. Transfer and approval amounts are encrypted in the event log, so standard event watchers see only ciphertext. seismic-viem ships two client extensions that filter these events and decrypt the amounts for you:

    • src20WalletActions.watchSRC20Events -- wallet-client action that fetches the viewing key for the connected address from the Directory contract via a signed read, then watches and decrypts events the wallet can read.

    • src20PublicActions.watchSRC20EventsWithKey -- public-client action that takes an explicit AES viewing key. Useful for server-side monitoring, intelligence providers, or any flow without a connected wallet.

    PrivateKey

    encryption_private_key

    PrivateKey

    Client's 32-byte secp256k1 private key

    metadata

    TxSeismicMetadata

    Transaction metadata (used to build AAD)

    metadata

    TxSeismicMetadata

    Transaction metadata (used to build AAD)

    encrypt()
    decrypt()
    EncryptionNonce
    TxSeismicMetadata
    Shielded Write Guide
    Bytes32
    HKDFarrow-up-right
    CompressedPublicKey
    EncryptionNonce
    EncryptionNonce

    Contract ABI

    functionName

    string

    Yes

    Function to call

    args

    array

    No

    Function arguments

    gas

    bigint

    No

    Gas limit

    gasPrice

    bigint

    No

    Gas price

    value

    bigint

    No

    ETH value to send

    Construct a type 0x4A transaction with the encrypted calldata and Seismic-specific fields
  • Sign and broadcast the transaction

  • Override the encryption nonce

    recentBlockHash

    Hex

    Latest

    Override the recent block hash

    expiresAtBlock

    bigint

    Calculated

    Override the expiry block directly

    namespaces on
    getShieldedContract
    ). The smart-routing
    .read
    /
    .write
    and transparent
    .tread
    /
    .twrite
    paths do not accept it.
  • Shielded Wallet Client -- Creating the client used for shielded writes

  • address

    Hex

    Yes

    Contract address

    abi

    Abi

    blocksWindow

    bigint

    100n

    Number of blocks before the transaction expires

    encryptionNonce

    Hex

    getShieldedContract
    Contract Instance
    Signed Reads
    Encryption

    Yes

    Random

    import { shieldedWriteContract } from "seismic-viem";
    import { shieldedWriteContract } from "seismic-viem";
    
    const hash = await shieldedWriteContract(client, {
      address: "0x1234567890abcdef1234567890abcdef12345678",
      abi: myContractAbi,
      functionName: "transfer",
      args: ["0xRecipient...", 100n],
      gas: 100_000n,
    });
    import { shieldedWriteContractDebug } from "seismic-viem";
    
    const { plaintextTx, shieldedTx, txHash } = await shieldedWriteContractDebug(
      client,
      {
        address: "0x1234567890abcdef1234567890abcdef12345678",
        abi: myContractAbi,
        functionName: "transfer",
        args: ["0xRecipient...", 100n],
      },
    );
    
    console.log("Plaintext calldata:", plaintextTx.data);
    console.log("Encrypted calldata:", shieldedTx.data);
    console.log("Transaction hash:", txHash);
    const hash = await client.sendShieldedTransaction({
      to: "0x1234567890abcdef1234567890abcdef12345678",
      data: "0x...", // raw calldata
      value: 0n,
      gas: 100_000n,
      gasPrice: 1_000_000_000n,
    });
    const hash = await shieldedWriteContract(
      client,
      {
        address: "0x...",
        abi: myContractAbi,
        functionName: "transfer",
        args: ["0x...", 100n],
      },
      {
        blocksWindow: 50n, // expires after 50 blocks instead of 100
      },
    );
    hashtag
    Signature

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    network_pk

    Yes

    The TEE's 33-byte compressed secp256k1 public key

    client_sk

    hashtag
    Returns

    Type
    Description

    Fully initialized encryption state with AES key and keypair

    hashtag
    Examples

    hashtag
    Basic Usage

    hashtag
    With Custom Client Key

    hashtag
    In Client Factory

    hashtag
    Random Ephemeral Key

    hashtag
    Verify Key Derivation

    hashtag
    How It Works

    The function performs three steps:

    1. Generate client key if needed

      if client_sk is None:
          client_sk = PrivateKey(os.urandom(32))
    2. Derive AES key via ECDH + HKDFarrow-up-right

      aes_key = generate_aes_key(client_sk, network_pk)

      This performs:

      • ECDH: Compute shared secret from client_sk and network_pk

      • HKDF: Derive 32-byte AES key from shared secret

    3. Derive client public key

    4. Return EncryptionState

    hashtag
    ECDH Key Exchange

    The ECDH key exchange works as follows:

    The client sends client_pk in the transaction's SeismicElements, allowing the TEE to derive the same AES key and decrypt the calldata.

    hashtag
    Random vs Deterministic Keys

    Key Type
    Pros
    Cons

    Random ephemeral

    No key management needed, fresh key per session

    Cannot recreate encryption state, no key persistence

    Deterministic

    Can recreate same state, key persistence, backup via mnemonic

    Requires secure key storage, key management complexity

    The SDK defaults to random ephemeral keys for simplicity. Use deterministic keys only if you need to recreate the same encryption state across sessions.

    hashtag
    Notes

    • Pure computation - no RPC calls or I/O

    • Works in both sync and async contexts

    • If client_sk is None, generates a cryptographically secure random key via os.urandom(32)

    • The AES key is derived using ECDH + HKDF (NIST SP 800-56C)

    • Called automatically by and

    • You rarely need to call this directly unless implementing custom client logic

    hashtag
    Security Considerations

    • Random key generation - Uses os.urandom() which is cryptographically secure on all platforms

    • Forward secrecy - Each encryption session can use a different ephemeral key

    • Key storage - If using deterministic keys, store client_sk securely (encrypted at rest, never logged)

    • ECDH security - Based on secp256k1 elliptic curve discrete logarithm problem

    • HKDF - Uses SHA-256 for key derivation

    hashtag
    Common Patterns

    hashtag
    Ephemeral Session Keys

    hashtag
    Persistent Keys

    hashtag
    Rotate Keys

    hashtag
    See Also

    • EncryptionState - Returned encryption state class

    • create_wallet_client - Sync client factory (calls get_encryption)

    • create_async_wallet_client - Async client factory

    • - Client private key type

    • - TEE public key type

    • - How encryption is used in transactions

    EncryptionState
    def get_encryption(
        network_pk: CompressedPublicKey,
        client_sk: PrivateKey | None = None,
    ) -> EncryptionState
    from seismic_web3 import get_encryption, PrivateKey, CompressedPublicKey
    
    # Get TEE public key (from node)
    tee_pk = CompressedPublicKey("0x02abcd...")
    
    # Derive encryption state (random ephemeral key)
    encryption = get_encryption(tee_pk)
    
    print(f"AES key: {encryption.aes_key.to_0x_hex()}")
    print(f"Client pubkey: {encryption.encryption_pubkey.to_0x_hex()}")
    import os
    from seismic_web3 import get_encryption, PrivateKey, CompressedPublicKey
    
    tee_pk = CompressedPublicKey("0x02abcd...")
    
    # Use a deterministic client key
    client_sk = PrivateKey.from_hex_str(os.environ["CLIENT_KEY"])
    
    encryption = get_encryption(tee_pk, client_sk)
    import os
    from seismic_web3 import get_encryption, get_tee_public_key, PrivateKey
    from web3 import Web3
    
    # This is what create_wallet_client() does internally
    w3 = Web3(Web3.HTTPProvider("https://testnet-1.seismictest.net/rpc"))
    
    # Step 1: Fetch TEE public key
    network_pk = get_tee_public_key(w3)
    
    # Step 2: Derive encryption state
    signing_key = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
    encryption = get_encryption(network_pk, client_sk=None)  # Random ephemeral key
    
    # Step 3: Attach to client
    # w3.seismic = SeismicNamespace(w3, encryption, signing_key)
    from seismic_web3 import get_encryption, CompressedPublicKey
    
    tee_pk = CompressedPublicKey("0x02abcd...")
    
    # Each call generates a new random key
    encryption1 = get_encryption(tee_pk)
    encryption2 = get_encryption(tee_pk)
    
    # Different keys
    assert encryption1.encryption_private_key != encryption2.encryption_private_key
    assert encryption1.aes_key != encryption2.aes_key
    import os
    from seismic_web3 import get_encryption, PrivateKey, CompressedPublicKey
    from seismic_web3.crypto.secp import private_key_to_compressed_public_key
    
    tee_pk = CompressedPublicKey("0x02abcd...")
    client_sk = PrivateKey.from_hex_str(os.environ["CLIENT_KEY"])
    
    encryption = get_encryption(tee_pk, client_sk)
    
    # Verify public key derivation
    expected_pubkey = private_key_to_compressed_public_key(client_sk)
    assert encryption.encryption_pubkey == expected_pubkey
    
    # Verify keys are stored correctly
    assert encryption.encryption_private_key == client_sk
    Client has:     client_sk (private), client_pk (public)
    TEE has:        tee_sk (private), tee_pk (public)
    
    Client computes:  shared_secret = ECDH(client_sk, tee_pk)
    TEE computes:     shared_secret = ECDH(tee_sk, client_pk)
    
    Both derive:      aes_key = HKDF(shared_secret)
    from seismic_web3 import get_encryption, CompressedPublicKey
    
    # New random key for each session (recommended)
    def create_session_encryption(tee_pk: CompressedPublicKey):
        return get_encryption(tee_pk)  # Random key
    from seismic_web3 import get_encryption, PrivateKey, CompressedPublicKey
    import os
    
    # Load persisted key from secure storage
    def load_encryption(tee_pk: CompressedPublicKey):
        client_sk = PrivateKey.from_hex_str(os.environ["ENCRYPTION_KEY"])
        return get_encryption(tee_pk, client_sk)
    from seismic_web3 import get_encryption, PrivateKey, CompressedPublicKey
    
    # Rotate to a new key periodically
    def rotate_encryption_key(tee_pk: CompressedPublicKey):
        new_client_sk = PrivateKey(os.urandom(32))
        return get_encryption(tee_pk, new_client_sk)

    Both actions are already applied by createShieldedWalletClient / createShieldedPublicClient; you can also extend a vanilla viem client with them manually.

    hashtag
    How Decryption Works

    1. An account registers its AES viewing key with the Directory contract.

    2. When the SRC20 contract emits Transfer or Approval, the log carries:

      • encryptKeyHash -- keccak256(viewingKey), allowing on-chain filtering

      • encryptedAmount -- the AES-GCM ciphertext of the amount

    3. The watcher subscribes with a filter on encryptKeyHash so it only receives events it can decrypt.

    4. For each matching log, the client decrypts encryptedAmount with the AES key and invokes the user callback with a DecryptedTransferLog / DecryptedApprovalLog.

    hashtag
    Import

    hashtag
    Wallet Action: watchSRC20Events

    Watches events for the connected wallet. The viewing key is fetched automatically from the Directory contract via a signed read.

    Parameter
    Type
    Required
    Description

    address

    Address

    Yes

    SRC20 token contract address

    onTransfer

    (log: DecryptedTransferLog) => void

    Returns: Promise<() => void> -- call the returned function to stop watching.

    circle-exclamation

    Throws if no AES key is registered in the Directory contract for the connected address. Register a viewing key first (typically during wallet setup) before watching.

    hashtag
    Public Action: watchSRC20EventsWithKey

    Same filter + decryption flow, but takes an explicit viewing key instead of fetching it from the Directory.

    Parameter
    Type
    Required
    Description

    viewingKey

    Hex

    Yes

    32-byte AES key used to decrypt amounts

    params

    WatchSRC20EventsParams

    Returns: Promise<() => void> -- call the returned function to stop watching.

    hashtag
    Log Types

    hashtag
    See Also

    • Shielded Public Client -- base client that includes watchSRC20EventsWithKey

    • Shielded Wallet Client -- base client that includes watchSRC20Events

    • Encrypted Events tutorial -- end-to-end SRC20 event walkthrough

    import {
      src20PublicActions,
      src20WalletActions,
    } from "seismic-viem";
    const unwatch = await walletClient.watchSRC20Events({
      address: "0xYourTokenAddress",
      onTransfer: (log) => {
        console.log(`Transfer ${log.from} -> ${log.to}: ${log.decryptedAmount}`);
      },
      onApproval: (log) => {
        console.log(
          `Approval ${log.owner} -> ${log.spender}: ${log.decryptedAmount}`,
        );
      },
      onError: (err) => console.error("decrypt failed:", err),
    });
    
    // stop watching
    unwatch();
    const viewingKey: Hex = "0x..."; // 32-byte AES key
    
    const unwatch = await publicClient.watchSRC20EventsWithKey(viewingKey, {
      address: "0xYourTokenAddress",
      onTransfer: (log) => console.log(log),
      onApproval: (log) => console.log(log),
      onError: (err) => console.error(err),
    });
    type DecryptedTransferLog = {
      from: Address;
      to: Address;
      encryptKeyHash: Hex; // keccak256 of the viewing key
      encryptedAmount: Hex; // original AES-GCM ciphertext
      decryptedAmount: bigint; // plaintext amount
      transactionHash: Hex;
      blockNumber: bigint;
    };
    
    type DecryptedApprovalLog = {
      owner: Address;
      spender: Address;
      encryptKeyHash: Hex;
      encryptedAmount: Hex;
      decryptedAmount: bigint;
      transactionHash: Hex;
      blockNumber: bigint;
    };

    Storage

    hashtag
    How Shielded Storage Works

    Seismic extends the EVM storage model with FlaggedStorage. Every storage slot is represented as a pair:

    (value: U256, is_private: bool)

    The is_private flag determines whether a slot holds public or confidential data. This flag is set automatically by the compiler based on the types you use, and enforced at the opcode level:

    • SSTORE / SLOAD operate on public storage slots (the standard EVM behavior).

    • CSTORE (0xB1) / CLOAD (0xB0) operate on confidential storage slots.

    When you declare a shielded variable (e.g., suint256), the compiler generates CSTORE and CLOAD instructions instead of SSTORE and SLOAD. This happens automatically -- you do not need to manage opcodes yourself.

    hashtag
    Access Control Rules

    The FlaggedStorage model enforces strict separation between public and confidential data:

    Operation
    Result

    This means that if an external contract or observer uses SLOAD to read a shielded storage slot, the operation will revert. CLOAD can access both private and public slots — the compiler generates CLOAD for all shielded type access.

    hashtag
    Whole Slot Consumption

    Shielded types consume an entire 32-byte storage slot, regardless of their actual size. A suint64, which only needs 8 bytes, still occupies a full slot.

    This is a deliberate design choice. In standard Solidity, the compiler packs multiple small variables into a single slot to save gas. With shielded types, packing is not done for two reasons: a storage slot must be entirely private or entirely public (mixing would break the confidentiality model), and packing would leak the size of the shielded value (see below).

    hashtag
    Storage Layout Comparison

    In standard Solidity, small types are packed together:

    With shielded types, each field gets its own slot:

    This means shielded contracts consume more storage slots than their unshielded equivalents. Plan your contract's storage layout accordingly.

    hashtag
    Gas Considerations

    CLOAD and CSTORE have a constant gas cost regardless of the value being read or written. This is a critical privacy property.

    In the standard EVM, certain storage operations can have variable gas costs (e.g., writing a nonzero value to a slot that previously held zero costs more than overwriting an existing nonzero value). If CLOAD and CSTORE had similar variable costs, an observer could infer information about shielded values by analyzing gas consumption.

    By making gas costs constant, Seismic prevents this class of information leakage. No matter what value is being stored or loaded, the gas cost is the same.

    circle-exclamation

    While CLOAD and CSTORE themselves have constant gas cost, other operations on shielded values (such as loops, conditionals, and exponentiation) can still leak information through gas. See for details.

    hashtag
    Manual Slot Packing

    If you need to pack multiple shielded values into a single slot for efficiency, you can do so using inline assembly. However, this is an advanced technique and carries significant risk.

    When using inline assembly for slot packing:

    • You must ensure all values packed into a single slot share the same confidentiality level.

    • Incorrect packing can introduce vulnerabilities where private data is partially exposed or corrupted.

    • The compiler cannot verify the correctness of your assembly-level storage operations.

    triangle-exclamation

    Manual slot packing bypasses compiler safety checks. Use it only when absolutely necessary, and audit thoroughly. A mistake here can silently break your contract's privacy guarantees.

    hashtag
    Future Improvements

    Compiler-level slot packing for shielded types is planned for a future release. This will allow the compiler to automatically pack multiple shielded values of compatible sizes into a single confidential slot, reducing storage costs without requiring manual assembly.

    Until then, each shielded variable consumes its own full slot, and manual packing via assembly is the only alternative.

    No

    Called for each decrypted Transfer event

    onApproval

    (log: DecryptedApprovalLog) => void

    No

    Called for each decrypted Approval event

    onError

    (error: Error) => void

    No

    Called when decryption fails

    Yes

    Same shape as watchSRC20Events above

    No

    Optional 32-byte client private key. If None, a random ephemeral key is generated

    create_wallet_client()
    create_async_wallet_client()
    PrivateKey
    CompressedPublicKey
    Shielded Write Guide
    CompressedPublicKey
    PrivateKey
    EncryptionState

    Returns the value

    SSTORE to a public slot

    Marks the slot as public

    SSTORE to a private slot

    Reverts

    CSTORE to a private slot

    Marks the slot as private

    CSTORE to a zero-value public slot

    Claims the slot as private

    CSTORE to a non-zero public slot

    Reverts

    SLOAD on a public slot

    Returns the value

    SLOAD on a private slot

    Reverts

    CLOAD on a private slot

    Returns the value

    Gas Considerations
    Be Mindful of Gas-Based Information Leakage

    CLOAD on a public slot

    client_pubkey = private_key_to_compressed_public_key(client_sk)
    return EncryptionState(
        aes_key=aes_key,
        encryption_pubkey=client_pubkey,
        encryption_private_key=client_sk,
    )
    contract RegularStorage {
        struct RegularStruct {
            uint64 a;   // Slot 0 (packed)
            uint128 b;  // Slot 0 (packed)
            uint64 c;   // Slot 0 (packed)
        }
    
        RegularStruct regularData;
    
        /*
           Storage Layout:
           - Slot 0: [a | b | c]
        */
    }
    contract ShieldedStorage {
        struct ShieldedStruct {
            suint64 a;  // Slot 0
            suint128 b; // Slot 1
            suint64 c;  // Slot 2
        }
    
        ShieldedStruct shieldedData;
    
        /*
           Storage Layout:
           - Slot 0: [a]
           - Slot 1: [b]
           - Slot 2: [c]
        */
    }
    contract ManualSlotPacking {
        // Use a deterministic slot derived from a namespace string to avoid collisions.
        // keccak256("ManualSlotPacking.packed") = a fixed slot number.
    
        function _packedSlot() internal pure returns (uint256 s) {
            assembly {
                s := keccak256(0, 0)  // placeholder, we use a constant below
            }
            // Use a constant derived from a namespace to avoid storage collisions.
            s = uint256(keccak256("ManualSlotPacking.packed"));
        }
    
        function packTwo(suint128 a, suint128 b) public {
            uint256 slot = _packedSlot();
            assembly {
                let packed := or(shl(128, a), and(b, 0xffffffffffffffffffffffffffffffff))
                cstore(slot, packed)
            }
        }
    
        function unpackTwo() public view returns (uint128, uint128) {
            uint256 slot = _packedSlot();
            uint256 packed;
            assembly {
                packed := cload(slot)
            }
            uint128 a = uint128(packed >> 128);
            uint128 b = uint128(packed);
            return (a, b);
        }
    }

    Precompiles

    Seismic precompiled contracts for cryptographic operations

    Seismic extends the EVM with precompiled contracts for common cryptographic operations. These are available at fixed addresses and can be called from any client with a .call() method -- either a ShieldedPublicClient or a ShieldedWalletClient.

    hashtag
    Import

    import {
      rng,
      ecdh,
      aesGcmEncrypt,
      aesGcmDecrypt,
      hdfk,
      secp256k1Sig,
    } from "seismic-viem";

    hashtag
    Overview

    Precompile
    Address
    Description
    Input
    Output
    circle-info

    Precompile calls execute within the TEE, ensuring cryptographic operations are performed in a secure environment. The inputs and outputs are transmitted over the encrypted channel established during client construction.

    hashtag
    RNG -- Random Number Generation

    Generates cryptographically secure random numbers using the TEE's CSPRNG.

    hashtag
    Standalone Function

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns

    bigint -- the generated random value.

    hashtag
    Example


    hashtag
    ECDH -- Elliptic Curve Diffie-Hellman

    Performs an ECDH key exchange inside the TEE and returns the shared secret.

    hashtag
    Standalone Function

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns

    Hex -- 32-byte shared secret.

    hashtag
    Example


    hashtag
    AES-GCM Encrypt / Decrypt

    Performs AES-GCM encryption and decryption inside the TEE.

    hashtag
    Encrypt

    hashtag
    Encrypt Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Encrypt Returns

    Hex -- the AES-GCM ciphertext.

    hashtag
    Decrypt

    hashtag
    Decrypt Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Decrypt Returns

    string -- the decrypted plaintext.

    hashtag
    Full Example


    hashtag
    HKDF -- Key Derivation

    Derives a key from input key material using HKDF inside the TEE.

    hashtag
    Standalone Function

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns

    Hex -- the derived key.

    hashtag
    Example


    hashtag
    secp256k1 Signature

    Generates a secp256k1 signature inside the TEE.

    hashtag
    Standalone Function

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Returns

    Signature -- a viem Signature object with r, s, and v components.

    hashtag
    Example


    hashtag
    Using Precompiles via Client

    All precompiles are available as methods directly on ShieldedPublicClient and ShieldedWalletClient, so you can call them without importing the standalone functions.

    Client Method
    Standalone Function
    Description

    hashtag
    Example

    hashtag
    Custom Precompile Pattern

    Each precompile is defined as a Precompile<P, R> object with a standard interface. You can invoke any precompile directly using its object.

    hashtag
    Precompile<P, R> Type

    Property
    Type
    Description

    hashtag
    Available Precompile Objects

    Export
    Type

    The CallClient type accepts any client with a .call() method, so both ShieldedPublicClient and ShieldedWalletClient work with callPrecompile.

    hashtag
    Calling via callPrecompile

    callPrecompile handles gas cost calculation, parameter encoding, and result decoding automatically using the Precompile<P, R> object passed in. It throws if no data is returned, if the arguments fail validation, or if encoding/decoding fails.

    hashtag
    See Also

    • -- Read-only client with precompile methods

    • -- Full-featured client with precompile methods

    • -- Client-side ECDH key exchange and AES-GCM encryption

    Encrypted Events

    Emit transfer events with encrypted amounts using AES precompiles

    In the previous chapter, we cast suint256 amounts to uint256 before emitting events. This works, but it reveals the amount in the public event log. This chapter shows how to encrypt event data so that only the intended recipients can read it. Estimated time: ~20 minutes.

    hashtag
    The problem

    Events in Ethereum (and Seismic) are stored in transaction logs, which are public. You cannot use shielded types directly in event parameters:

    // This will NOT compile
    event Transfer(address indexed from, address indexed to, suint256 amount);

    The compiler rejects this because event data is written to public logs, and shielded types are only meaningful in contract storage. If you cast to uint256 and emit, the amount appears in plaintext in the log -- defeating the purpose of shielding it in the first place.

    hashtag
    The solution

    Use Seismic's AES-GCM precompiles to encrypt the sensitive data before emitting it. The event carries opaque bytes that only the intended recipient can decrypt.

    The modified event signature uses bytes instead of uint256 for the amount:

    The from and to addresses remain as indexed parameters. These are public -- observers can see who is transacting with whom. Only the amount is encrypted. If you need to hide the participants as well, you can encrypt those too, but that is less common for a token.

    hashtag
    Step by step

    The encryption flow uses three of Seismic's precompiles:

    hashtag
    Step 1: Derive a shared secret with ECDH

    The ECDH precompile at address 0x65 performs Elliptic Curve Diffie-Hellman key agreement. Given a private key and a public key, it produces a shared secret that both parties can independently derive.

    For event encryption, the contract needs a keypair. The private key must not be passed as a constructor argument -- constructor calldata is not encrypted (CREATE/CREATE2 transactions are standard Ethereum transaction types), so the key would leak in the deployment data.

    Instead, set the key after deployment via a Seismic transaction (type 0x4A), which encrypts calldata:

    Because setContractKey is called via a Seismic transaction, the private key is encrypted in the calldata before it leaves the caller's machine. The key is stored as sbytes32, so the compiler routes it through shielded storage — observers see 0x00...0 instead of the actual key.

    To derive a shared secret with a specific recipient, the contract calls the ECDH precompile with its own private key and the recipient's public key:

    hashtag
    Step 2: Derive an encryption key with HKDF

    The raw ECDH shared secret should not be used directly as an encryption key. The HKDF precompile at address 0x68 derives a proper cryptographic key from the shared secret:

    The second argument is a context string (sometimes called "info" in HKDF terminology). Using a unique context string for each purpose ensures that the same shared secret produces different keys for different uses.

    hashtag
    Step 3: Encrypt with AES-GCM

    The AES-GCM Encrypt precompile at address 0x66 encrypts the data:

    hashtag
    Step 4: Emit the encrypted event

    Putting it all together in an internal helper:

    hashtag
    Full implementation

    Here is the updated transfer function using encrypted events:

    Users register their public key by calling registerPublicKey once. After that, any transfer they receive will emit an event encrypted to their key.

    hashtag
    Decrypting off-chain

    The recipient can decrypt the event data by performing the reverse of the encryption flow:

    1. Take the contract's public key (stored on-chain and readable by anyone).

    2. Combine it with their own private key using ECDH to derive the same shared secret.

    3. Run HKDF with the same context string ("src20-transfer-event") to derive the same encryption key.

    Decrypt events client-side using the ECDH shared secret and AES-GCM decryption. See the for the cryptographic primitives.

    hashtag
    Who can read what

    Here is the visibility breakdown for each piece of data in a Transfer event:

    Data
    Who can see it
    Why

    The sender can also decrypt the amount because they know the plaintext -- they created the transaction. If you need the sender to be able to decrypt from the event log as well (for example, for transaction history), you can emit a second event encrypted to the sender's key, or encrypt to both keys and include both ciphertexts.

    circle-info

    Encrypted events add gas cost for the precompile calls. For applications where the event amount being public is acceptable, the simpler uint256(amount) cast from the previous chapter is more gas-efficient. Choose the approach that matches your privacy requirements.

    Signed Reads

    Authenticated read calls that prove caller identity

    On standard Ethereum, eth_call can spoof any from address -- there is no signature check. Seismic prevents this: all unsigned eth_call requests have msg.sender set to the zero address. To read shielded data that depends on msg.sender, you need a signed read: a signed transaction submitted to eth_call that proves the caller's identity.

    hashtag
    Why Signed Reads Matter

    Contracts can use msg.sender in view functions to gate access to shielded data. A common example: a token contract with a balanceOf() that takes no arguments and uses msg.sender internally to look up the caller's balance. Without a signed read, the contract sees the zero address and returns that address's balance -- which is almost certainly zero.

    seismic-viem provides several approaches:

    • contract.read.functionName() -- smart routing via . Auto-detects shielded parameters and uses signed read only when needed.

    • contract.sread.functionName() -- force signed read via . Always uses signed read regardless of parameter types.

    The signed read paths all encrypt the calldata, sign it, and decrypt the response automatically.


    hashtag
    Standalone: signedReadContract

    hashtag
    Parameters

    signedReadContract(client, parameters, securityParams?)

    Parameter
    Type
    Required
    Description

    The optional third argument securityParams: SeismicSecurityParams accepts advanced Seismic metadata overrides (see ). Most callers should omit these; they are mainly useful for deterministic tests/debugging, explicit expiry control, and low-level interop.

    hashtag
    Returns

    Promise<ReadContractReturnType> -- the decoded return value, typed according to the ABI.

    circle-info

    If the client has no account configured, signedReadContract falls back to a standard readContract (transparent eth_call). Pass an account-bearing ShieldedWalletClient to get the signed read behavior.

    hashtag
    Example


    hashtag
    Low-level: signedCall

    For cases where you have raw calldata instead of ABI-encoded parameters -- for example, pre-encoded data or non-ABI interactions:

    signedCall also accepts SeismicSecurityParams as a third argument for overriding the encryption nonce, block hash, or expiry window. See for the full parameter table.


    hashtag
    How It Works

    When you call signedReadContract (or contract.sread.functionName), the SDK performs the following steps:

    1. ABI-encode the function call into plaintext calldata

    2. Build Seismic metadata with signedRead: true

    3. Encrypt calldata with AES-GCM using the shared key derived via ECDH

    Both the calldata you send and the result you receive are encrypted. An observer watching the network can see that a call was made to a particular contract address, but not what function was called or what was returned.


    hashtag
    Signed Read vs Transparent Read

    circle-info

    The smart .read namespace handles most cases correctly by inspecting the ABI. Use .sread when you need the response encrypted even though the function has no shielded input parameters. Use .tread only for public data where you don't need authentication.

    circle-exclamation

    .tread and walletClient.treadContract reject the account option and will throw. On Seismic, transparent eth_call zeroes out from on the node, so any account you pass would silently be ignored. For any sender-aware read, use .sread / sreadContract.

    hashtag
    See Also

    • -- getShieldedContract with .read and .tread namespaces

    • -- Encrypted write transactions using the same pipeline

    • -- ECDH key exchange and AES-GCM details

    0x...0065

    ECDH key exchange

    sk, pk

    Hex (shared secret)

    aesGcmEncrypt

    0x...0066

    AES-GCM encryption

    aesKey, nonce, plaintext

    Hex (ciphertext)

    aesGcmDecrypt

    0x...0067

    AES-GCM decryption

    aesKey, nonce, ciphertext

    string (plaintext)

    hdfk

    0x...0068

    HKDF key derivation

    ikm

    Hex (derived key)

    secp256k1Sig

    0x...0069

    secp256k1 signing

    sk, message

    Signature

    Personalization string to seed the CSPRNG

    33-byte compressed secp256k1 public key

    Numeric nonce for AES-GCM

    plaintext

    string

    Yes

    Plaintext string to encrypt

    Nonce used during encryption

    ciphertext

    Hex

    Yes

    Ciphertext to decrypt

    Message to sign

    aesGcmEncrypt(client, params)

    AES-GCM encryption

    client.aesGcmDecryption(params)

    aesGcmDecrypt(client, params)

    AES-GCM decryption

    client.hdfk(ikm)

    hdfk(client, ikm)

    HKDF key derivation

    client.secp256k1Signature(params)

    secp256k1Sig(client, params)

    secp256k1 signing

    (args: P) => Hex

    ABI-encodes the arguments for the call

    decodeResult(result)

    (result: Hex) => R

    Decodes the raw call result

    hdfkPrecompile

    Precompile<Hex, Hex>

    secp256k1SigPrecompile

    Precompile<Secp256K1SigParams, Signature>

    Chains -- Chain configurations for Seismic networks

    rng

    0x...0064

    Random number generation

    numBytes, pers?

    bigint

    numBytes

    bigint | number

    Yes

    Number of random bytes to generate (1--32)

    pers

    Hex | ByteArray

    sk

    Hex

    Yes

    32-byte secp256k1 secret key

    pk

    Hex

    aesKey

    Hex

    Yes

    32-byte AES-256 key

    nonce

    number

    aesKey

    Hex

    Yes

    32-byte AES-256 key

    nonce

    number

    ikm

    Hex

    Yes

    Input key material

    sk

    Hex

    Yes

    32-byte secp256k1 secret key

    message

    string

    client.rng(params)

    rng(client, params)

    Random number generation

    client.ecdh(params)

    ecdh(client, params)

    ECDH key exchange

    address

    Hex

    Fixed address of the precompile contract

    gasCost(args)

    (args: P) => bigint

    Computes the gas cost for the given arguments

    rngPrecompile

    Precompile<RngParams, bigint>

    ecdhPrecompile

    Precompile<EcdhParams, Hex>

    aesGcmEncryptPrecompile

    Precompile<AesGcmEncryptionParams, Hex>

    aesGcmDecryptPrecompile

    Shielded Public Client
    Shielded Wallet Client
    Encryption

    ecdh

    No

    Yes

    Yes

    Yes

    Yes

    client.aesGcmEncryption(params)

    encodeParams(args)

    Precompile<AesGcmDecryptionParams, string>

    Decrypt the encryptedAmount from the event log using AES-GCM Decrypt.

    Recipient only

    Encrypted to recipient's public key

    A transfer happened

    Everyone

    The event emission itself is visible

    from address

    Everyone

    Indexed parameter, stored in public log topics

    to address

    Everyone

    Indexed parameter, stored in public log topics

    seismic-viem precompiles documentation

    Transfer amount

    signedReadContract()
    -- standalone function, same API shape as viem's
    readContract
    . Always uses signed read.
  • walletClient.readContract() -- smart routing via the wallet client. Same auto-detection as contract.read.

  • walletClient.sreadContract() -- force signed read via the wallet client. Always uses signed read.

  • Contract ABI

    functionName

    string

    Yes

    Name of the view/pure function

    args

    array

    No

    Function arguments

    nonce

    number

    No

    Override the nonce

    Sign the transaction:
    • For local accounts (private key): sign as a raw Seismic transaction, send to eth_call

    • For JSON-RPC accounts (MetaMask): sign EIP-712 typed data via eth_signTypedData_v4, send the typed data + signature to eth_call

  • Decrypt the response returned by the node

  • Decode the ABI output into the expected return type

  • Always signer's address

    Always zero address

    Use case

    Default -- handles most cases

    Force encryption for sensitive returns

    Public view functions

    Performance

    Optimal -- encrypts only when needed

    Slightly slower (sign + encrypt + decrypt)

    Standard eth_call speed

  • Shielded Wallet Client -- Creating the client used for signed reads

  • address

    Hex

    Yes

    Contract address

    abi

    Abi

    Aspect

    .read (Smart)

    .sread (Force Signed)

    .tread (Transparent)

    Calldata

    Auto-detected by ABI

    Always encrypted

    Always plaintext

    msg.sender

    getShieldedContract
    getShieldedContract
    Security Parameters
    Shielded Writes
    Contract Instance
    Shielded Writes
    Encryption

    Yes

    Signer if shielded, zero if not

    import { rng } from "seismic-viem";
    
    const randomValue = await rng(client, {
      numBytes: 32,
    });
    
    console.log("Random value:", randomValue);
    import { createShieldedPublicClient } from "seismic-viem";
    import { rng } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    
    const client = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    
    // Generate 32 random bytes
    const randomValue = await rng(client, { numBytes: 32 });
    console.log("Random 256-bit value:", randomValue);
    
    // Generate 16 random bytes with a personalization string
    const seededValue = await rng(client, {
      numBytes: 16,
      pers: "0x6d79617070",
    });
    console.log("Seeded random value:", seededValue);
    import { ecdh } from "seismic-viem";
    
    const sharedSecret = await ecdh(client, {
      sk: "0x...", // 32-byte secret key
      pk: "0x...", // 33-byte compressed public key
    });
    
    console.log("Shared secret:", sharedSecret);
    import { ecdh } from "seismic-viem";
    
    const sharedSecret = await ecdh(client, {
      sk: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
      pk: "0x0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
    });
    
    console.log("Shared secret:", sharedSecret);
    import { aesGcmEncrypt } from "seismic-viem";
    
    const ciphertext = await aesGcmEncrypt(client, {
      aesKey: "0x...", // 32-byte AES key
      nonce: 1, // numeric nonce
      plaintext: "hello world",
    });
    
    console.log("Ciphertext:", ciphertext);
    import { aesGcmDecrypt } from "seismic-viem";
    
    const plaintext = await aesGcmDecrypt(client, {
      aesKey: "0x...", // same 32-byte AES key
      nonce: 1, // same nonce used for encryption
      ciphertext: "0x...",
    });
    
    console.log("Plaintext:", plaintext);
    import { aesGcmEncrypt, aesGcmDecrypt, rng } from "seismic-viem";
    
    // Generate a random AES key using the RNG precompile
    const aesKeyRaw = await rng(client, { numBytes: 32 });
    const aesKey = `0x${aesKeyRaw.toString(16).padStart(64, "0")}` as const;
    
    // Encrypt
    const ciphertext = await aesGcmEncrypt(client, {
      aesKey,
      nonce: 1,
      plaintext: "secret message",
    });
    
    // Decrypt
    const plaintext = await aesGcmDecrypt(client, {
      aesKey,
      nonce: 1,
      ciphertext,
    });
    
    console.log("Decrypted:", plaintext); // "secret message"
    import { hdfk } from "seismic-viem";
    
    const derivedKey = await hdfk(client, "0x..."); // input key material
    
    console.log("Derived key:", derivedKey);
    import { hdfk } from "seismic-viem";
    
    const inputKeyMaterial = "0xdeadbeef";
    const derivedKey = await hdfk(client, inputKeyMaterial);
    console.log("Derived key:", derivedKey);
    import { secp256k1Sig } from "seismic-viem";
    
    const signature = await secp256k1Sig(client, {
      sk: "0x...", // 32-byte secret key
      message: "hello", // message to sign
    });
    
    console.log("Signature:", signature);
    import { secp256k1Sig } from "seismic-viem";
    
    const signature = await secp256k1Sig(client, {
      sk: "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
      message: "sign this message",
    });
    
    console.log("r:", signature.r);
    console.log("s:", signature.s);
    console.log("v:", signature.v);
    import { createShieldedPublicClient } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    
    const client = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    
    // Call precompiles as client methods
    const randomValue = await client.rng({ numBytes: 32 });
    
    const sharedSecret = await client.ecdh({
      sk: "0x...",
      pk: "0x...",
    });
    
    const ciphertext = await client.aesGcmEncryption({
      aesKey: "0x...",
      nonce: 1,
      plaintext: "hello",
    });
    
    const derivedKey = await client.hdfk("0x...");
    import { callPrecompile, rngPrecompile } from "seismic-viem";
    
    // Access the precompile address directly
    console.log("RNG precompile address:", rngPrecompile.address);
    
    // Call any precompile by passing its object to callPrecompile
    const randomValue = await callPrecompile({
      client,
      precompile: rngPrecompile,
      args: { numBytes: 32 },
    });
    event Transfer(address indexed from, address indexed to, bytes encryptedAmount);
    address public owner;
    sbytes32 contractPrivateKey;
    bytes public contractPublicKey;
    
    constructor(
        string memory _name,
        string memory _symbol,
        uint256 _initialSupply
    ) {
        name = _name;
        symbol = _symbol;
        totalSupply = _initialSupply;
        balanceOf[msg.sender] = suint256(_initialSupply);
        owner = msg.sender;
    }
    
    /// @notice Call this immediately after deployment using a Seismic transaction.
    function setContractKey(bytes32 _privateKey, bytes memory _publicKey) external {
        require(msg.sender == owner, "Only owner");
        require(bytes32(contractPrivateKey) == bytes32(0), "Already set");
        contractPrivateKey = sbytes32(_privateKey);
        contractPublicKey = _publicKey;
    }
    function _deriveSharedSecret(bytes memory recipientPublicKey) internal view returns (sbytes32) {
        require(bytes32(contractPrivateKey) != bytes32(0), "Contract key not set");
        // Call ECDH precompile at 0x65
        // Note: private key comes FIRST, then public key
        (bool success, bytes memory result) = address(0x65).staticcall(
            abi.encodePacked(bytes32(contractPrivateKey), recipientPublicKey)
        );
        require(success, "ECDH failed");
        return sbytes32(abi.decode(result, (bytes32)));
    }
    function _deriveEncryptionKey(sbytes32 sharedSecret) internal view returns (sbytes32) {
        // Call HKDF precompile at 0x68
        // Pass raw key material bytes directly
        (bool success, bytes memory result) = address(0x68).staticcall(
            abi.encodePacked(bytes32(sharedSecret))
        );
        require(success, "HKDF failed");
        return sbytes32(abi.decode(result, (bytes32)));
    }
    function _encrypt(sbytes32 key, bytes12 nonce, bytes memory plaintext) internal view returns (bytes memory) {
        // Call AES-GCM Encrypt precompile at 0x66
        // Input format: key (32 bytes) + nonce (12 bytes) + plaintext
        (bool success, bytes memory ciphertext) = address(0x66).staticcall(
            abi.encodePacked(bytes32(key), nonce, plaintext)
        );
        require(success, "Encryption failed");
        return ciphertext;
    }
    function _emitEncryptedTransfer(
        address from,
        address to,
        suint256 amount,
        bytes memory recipientPublicKey
    ) internal {
        // Derive shared secret between contract and recipient
        sbytes32 sharedSecret = _deriveSharedSecret(recipientPublicKey);
    
        // Derive encryption key
        sbytes32 encKey = _deriveEncryptionKey(sharedSecret);
    
        // Encrypt the amount (nonce can be derived or generated per-event)
        bytes12 nonce = bytes12(keccak256(abi.encodePacked(from, to, block.number)));
        bytes memory plaintext = abi.encode(uint256(amount));
        bytes memory encryptedAmount = _encrypt(encKey, nonce, plaintext);
    
        // Emit with encrypted data
        emit Transfer(from, to, encryptedAmount);
    }
    // Mapping of address to their public key (registered on-chain)
    mapping(address => bytes) public publicKeys;
    
    function registerPublicKey(bytes memory pubKey) external {
        publicKeys[msg.sender] = pubKey;
    }
    
    function transfer(address to, suint256 amount) public returns (bool) {
        require(balanceOf[msg.sender] >= amount, "Insufficient balance");
        balanceOf[msg.sender] -= amount;
        balanceOf[to] += amount;
    
        // Encrypt amount for the recipient
        bytes memory recipientPubKey = publicKeys[to];
        if (recipientPubKey.length > 0) {
            _emitEncryptedTransfer(msg.sender, to, amount, recipientPubKey);
        } else {
            // Fallback: emit with zero if recipient has no registered key
            emit Transfer(msg.sender, to, bytes(""));
        }
    
        return true;
    }
    // Smart read -- balanceOf(saddress) has shielded param → signed read automatically
    const myBalance = await contract.read.balanceOf(["0x1234..."]);
    
    // Smart read -- totalSupply() has no shielded params → transparent read automatically
    const totalSupply = await contract.read.totalSupply();
    
    // Force signed read -- always encrypted, even for non-shielded functions
    const supply = await contract.sread.totalSupply();
    import { signedReadContract } from "seismic-viem";
    import { signedReadContract } from "seismic-viem";
    
    const balance = await signedReadContract(
      client,
      {
        address: "0x1234567890abcdef1234567890abcdef12345678",
        abi: myContractAbi,
        functionName: "balanceOf",
        args: ["0xMyAddress..."],
      },
      {
        blocksWindow: 50n, // optional: expire after 50 blocks instead of the default 100
      },
    );
    import { signedCall } from "seismic-viem";
    
    const result = await signedCall(client, {
      to: "0x1234567890abcdef1234567890abcdef12345678",
      data: "0x...", // raw calldata
      account: client.account,
      gas: 30_000_000n,
    });
    // Smart read -- auto-detects: balanceOf(saddress) → signed, totalSupply() → transparent
    const myBalance = await contract.read.balanceOf(["0x1234..."]);
    const totalSupply = await contract.read.totalSupply();
    
    // Force signed read -- always encrypted
    const supply = await contract.sread.totalSupply();
    
    // Force transparent read -- always plaintext
    const supply2 = await contract.tread.totalSupply();

    Chains

    Pre-configured chain definitions for Seismic networks

    seismic-viem provides pre-configured viem Chain objects with Seismic transaction formatters. These chain definitions include the correct chain IDs, RPC endpoints, and custom formatters needed for Seismic's type 0x4A transactions.

    hashtag
    Import

    import {
      seismicTestnet,
      sanvil,
      localSeismicDevnet,
      createSeismicDevnet,
    } from "seismic-viem";

    hashtag
    Available Chains

    Chain
    Export
    Chain ID
    RPC URL
    Description

    hashtag
    Seismic Testnet

    The public testnet for development and testing against a live Seismic network:

    hashtag
    Sanvil

    Local development chain using Sanvil (Seismic's fork of Anvil). Chain ID 31337 matches Anvil/Hardhat defaults:

    hashtag
    Local Devnet

    For running a local seismic-reth --dev node. Uses chain ID 5124 (same as testnet) but connects to localhost:

    hashtag
    Choosing a Chain

    hashtag
    SEISMIC_TX_TYPE

    All chain configs use the Seismic transaction type constant:

    The value 74 (0x4A) is the EIP-2718 transaction type envelope identifier for Seismic transactions. This constant is used internally by the chain formatters and encryption pipeline to identify and construct Seismic-specific transaction payloads.

    hashtag
    Chain Formatters

    Each chain config includes seismicChainFormatters -- custom viem chain formatters that handle Seismic transaction fields. These formatters are applied automatically when you use a Seismic chain definition.

    The formatters extend viem's standard transaction formatting to support the additional fields required by Seismic's type 0x4A transactions, including encryption metadata and signed read parameters.

    circle-info

    You do not need to configure or interact with seismicChainFormatters directly. They are embedded in every pre-configured chain object and in chains created via createSeismicDevnet.

    hashtag
    Custom Chain Factory

    hashtag
    createSeismicDevnet

    Create a custom chain definition for any Seismic-compatible node:

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    The factory returns a viem Chain object with:

    • Chain ID 5124

    • RPC URL set to {nodeHost}/rpc

    • Seismic chain formatters included

    hashtag
    Helper Factories

    Convenience factories for common Seismic infrastructure:

    These generate chain configs pointing to numbered Seismic testnet instances on Azure and GCP infrastructure respectively.

    hashtag
    SeismicTransactionRequest

    Seismic chain configs use a custom transaction request type that extends viem's standard TransactionRequest with Seismic-specific fields:

    circle-info

    You do not need to populate these fields manually. The shielded wallet client's encryption pipeline fills them automatically when sending transactions. They are documented here for reference and debugging purposes.

    Field
    Type
    Description

    hashtag
    See Also

    • -- Install seismic-viem and viem

    • -- Full SDK overview and architecture

    • Shielded Wallet Client -- Create a client using a chain config

    sanvil

    31337

    http://127.0.0.1:8545

    Local Seismic Anvil

    Local Devnet

    localSeismicDevnet

    5124

    http://127.0.0.1:8545

    Local seismic-reth --dev

    Block explorer URL for the chain

    Optional block explorer configuration

    number

    Version of the Seismic message format

    recentBlockHash

    Hex

    Recent block hash used for transaction replay protection

    expiresAtBlock

    bigint

    Block number after which the transaction becomes invalid

    signedRead

    boolean

    true for signed read requests, false for standard txs

    Encryption -- How calldata encryption uses chain-level formatters

    Seismic Testnet

    seismicTestnet

    5124

    https://testnet-1.seismictest.net/rpc

    Public testnet

    nodeHost

    string

    Yes

    Base URL of the Seismic node (without /rpc)

    explorerUrl

    string

    encryptionPubkey

    Hex

    Client's compressed secp256k1 public key

    encryptionNonce

    Hex

    Random nonce for AES-GCM encryption of calldata

    Installation
    Seismic Viem Overview

    Sanvil

    No

    messageVersion

    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { createShieldedWalletClient, seismicTestnet } from "seismic-viem";
    
    const client = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { createShieldedWalletClient, sanvil } from "seismic-viem";
    
    const client = await createShieldedWalletClient({
      chain: sanvil,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    import { createShieldedWalletClient, localSeismicDevnet } from "seismic-viem";
    
    const client = await createShieldedWalletClient({
      chain: localSeismicDevnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    Are you developing against a live network?
      -> Use seismicTestnet
    
    Are you running Sanvil locally for rapid iteration?
      -> Use sanvil
    
    Are you running seismic-reth --dev locally?
      -> Use localSeismicDevnet
    
    Do you need a custom chain configuration?
      -> Use createSeismicDevnet()
    import { SEISMIC_TX_TYPE } from "seismic-viem";
    
    console.log(SEISMIC_TX_TYPE); // 74 (0x4A)
    import { createSeismicDevnet } from "seismic-viem";
    
    const myChain = createSeismicDevnet({
      nodeHost: "https://my-seismic-node.example.com",
      explorerUrl: "https://explorer.example.com",
    });
    import { createSeismicAzTestnet, createSeismicGcpTestnet } from "seismic-viem";
    
    // Azure-hosted testnet instance N
    const azChain = createSeismicAzTestnet(1);
    
    // GCP-hosted testnet instance N
    const gcpChain = createSeismicGcpTestnet(1);
    interface SeismicTxExtras {
      encryptionPubkey: Hex; // Client's compressed secp256k1 public key
      encryptionNonce: Hex; // AES-GCM nonce for calldata encryption
      messageVersion: number; // Seismic message format version
      recentBlockHash: Hex; // Recent block hash for replay protection
      expiresAtBlock: bigint; // Block number at which the transaction expires
      signedRead: boolean; // Whether this is a signed read request
    }

    Ch 2: Contract Hooks

    In this chapter, you'll build the React hooks that connect your UI to the ClownBeatdown contract. These hooks encapsulate all contract interaction logic so your components stay clean. Estimated time: ~15 minutes

    hashtag
    Contract ABI setup

    First, copy the compiled ABI from your contracts build output into the web package. After deploying (see Deploying), copy the ABI file:

    mkdir -p packages/web/src/abis/contracts
    cp packages/contracts/out/ClownBeatdown.sol/ClownBeatdown.json \
       packages/web/src/abis/contracts/ClownBeatdown.json

    You'll also need to add the deployed contract address and chain ID to the JSON file. After copying, edit the file to include address and chainId at the top level. The final structure should look like:

    You can find the deployed address in packages/contracts/broadcast/ClownBeatdown.s.sol/31337/run-latest.json under transactions[0].contractAddress.

    hashtag
    Contract type definition

    Create src/types/contract.ts:

    hashtag
    useContract hook

    This hook creates a shielded contract instance using seismic-react. Create src/hooks/useContract.ts:

    The useShieldedContract hook from seismic-react returns a contract instance that supports both shielded writes and signed reads — the same interface you used in the CLI with getShieldedContract, but integrated with React's lifecycle.

    hashtag
    useContractClient hook

    This hook wraps the contract methods into callable functions with proper error handling. Create src/hooks/useContractClient.ts:

    hashtag
    What's happening here?

    Notice the different contract namespaces used for each method:

    • appContract().twrite.hit() and appContract().twrite.reset() — these are shielded write transactions. The twrite namespace sends a Seismic transaction (type 0x70) that encrypts calldata.

    • appContract().read.rob() — this is a signed read. The read namespace performs a

    This distinction between twrite, read, and tread is the key difference from a standard Ethereum dApp.

    hashtag
    Supporting components

    Before building the game actions hook, create the helper components and hooks it depends on.

    Explorer toast — Create src/components/chain/ExplorerToast.tsx:

    Toast notifications — Create src/hooks/useToastNotifications.ts:

    hashtag
    useGameActions hook

    This hook orchestrates the game logic, managing state and coordinating contract calls with UI feedback. Create src/hooks/useGameActions.ts:

    This hook manages the full game lifecycle:

    • fetchGameRounds — reads the current stamina from the contract via tread.getClownStamina()

    • handleHit — sends a shielded write via twrite.hit(), shows toast notifications with explorer links, waits for the receipt, increments punch count, and refetches stamina

    signed_call
    that proves the caller's identity to the contract, allowing
    onlyContributor
    to verify access. The result comes back as
    Hex
    and is decoded with
    hexToString()
    .
  • appContract().tread.getClownStamina() — this is a transparent read. The tread namespace performs a standard eth_call since stamina is public state.

  • handleReset — validates the clown is KO, sends a shielded write via twrite.reset(), clears punch count and rob result, and refetches stamina
  • handleRob — performs a signed read via read.rob() to decrypt and reveal a secret from the clown's pool

  • resetGameState — clears the rob result and punch count when the round changes

  • {
      "address": "0xYourDeployedAddress",
      "chainId": 31337,
      "abi": [
        { "type": "constructor", "inputs": [...] },
        { "type": "function", "name": "hit", ... },
        ...
      ]
    }
    export type ContractInterface = {
      chainId: number;
      abi: Array<Record<string, unknown>>;
      methodIdentifiers: Record<string, string>;
    };
    
    export type DeployedContract = ContractInterface & {
      address: `0x${string}`;
    };
    import { useShieldedContract } from "seismic-react";
    
    import * as contractJson from "@/abis/contracts/ClownBeatdown.json" with { type: "json" };
    import type { DeployedContract } from "@/types/contract";
    
    export const useAppContract = () =>
      useShieldedContract(contractJson as DeployedContract);
    import { useCallback, useEffect, useState } from "react";
    import { useShieldedWallet } from "seismic-react";
    import {
      type ShieldedPublicClient,
      type ShieldedWalletClient,
      addressExplorerUrl,
      txExplorerUrl,
    } from "seismic-viem";
    import { type Hex, hexToString } from "viem";
    
    import { useAppContract } from "./useContract";
    
    export const useContractClient = () => {
      const [loaded, setLoaded] = useState(false);
      const { walletClient, publicClient } = useShieldedWallet();
      const { contract } = useAppContract();
    
      useEffect(() => {
        if (walletClient && publicClient && contract) {
          setLoaded(true);
        } else {
          setLoaded(false);
        }
      }, [walletClient, publicClient, contract]);
    
      const wallet = useCallback((): ShieldedWalletClient => {
        if (!walletClient) {
          throw new Error("Wallet client not found");
        }
        return walletClient;
      }, [walletClient]);
    
      const pubClient = useCallback((): ShieldedPublicClient => {
        if (!publicClient) {
          throw new Error("Public client not found");
        }
        return publicClient;
      }, [publicClient]);
    
      const walletAddress = useCallback((): Hex => {
        return wallet().account.address;
      }, [wallet]);
    
      const appContract = useCallback((): ReturnType<
        typeof useAppContract
      >["contract"] => {
        if (!contract) {
          throw new Error("Contract not found");
        }
        return contract;
      }, [contract]);
    
      /*
        function getClownStamina() external view returns (uint256);
        function rob() external view returns (bytes32);
        function hit() external;
        function reset() external;
      */
    
      const clownStamina = useCallback(async (): Promise<bigint> => {
        return appContract().tread.getClownStamina();
      }, [appContract]);
    
      const rob = useCallback(async (): Promise<string> => {
        const result = (await appContract().read.rob()) as Hex;
        return hexToString(result);
      }, [appContract]);
    
      const hit = useCallback(async (): Promise<Hex> => {
        return appContract().twrite.hit();
      }, [appContract]);
    
      const reset = useCallback(async (): Promise<Hex> => {
        return appContract().twrite.reset();
      }, [appContract]);
    
      const txUrl = useCallback(
        (txHash: Hex): string | null => {
          return txExplorerUrl({ chain: pubClient().chain, txHash });
        },
        [pubClient],
      );
    
      const addressUrl = useCallback(
        (address: Hex): string | null => {
          return addressExplorerUrl({ chain: pubClient().chain, address });
        },
        [pubClient],
      );
    
      const waitForTransaction = useCallback(
        async (hash: Hex) => {
          return await pubClient().waitForTransactionReceipt({ hash });
        },
        [pubClient],
      );
    
      return {
        loaded,
        walletClient,
        publicClient,
        walletAddress,
        appContract,
        pubClient,
        wallet,
        clownStamina,
        rob,
        hit,
        reset,
        txUrl,
        addressUrl,
        waitForTransaction,
      };
    };
    import React from 'react'
    
    type ExplorerToastProps = {
      url: string
      text: string
      hash: string
    }
    
    export const ExplorerToast: React.FC<ExplorerToastProps> = ({ url, text, hash }) => (
      <a href={url} target="_blank" rel="noopener noreferrer">
        {text}{hash.slice(0, 10)}...
      </a>
    )
    import { toast } from 'react-toastify'
    
    export const useToastNotifications = () => ({
      notifySuccess: (msg: string) => toast.success(msg),
      notifyError: (msg: string) => toast.error(msg),
      notifyInfo: (msg: string | React.ReactElement) => toast.info(msg),
    })
    import { useCallback, useEffect, useState } from "react";
    import React from "react";
    import { useSound } from "use-sound";
    
    import { ExplorerToast } from "@/components/chain/ExplorerToast";
    import { useContractClient } from "@/hooks/useContractClient";
    import { useToastNotifications } from "@/hooks/useToastNotifications";
    
    export const useGameActions = () => {
      const [clownStamina, setClownStamina] = useState<number | null>(null);
      const [currentRoundId] = useState<number | null>(1);
    
      const {
        loaded,
        hit,
        rob,
        reset,
        txUrl,
        waitForTransaction,
        clownStamina: readClownStamina,
      } = useContractClient();
    
      const { notifySuccess, notifyError, notifyInfo } = useToastNotifications();
      const [isHitting, setIsHitting] = useState(false);
      const [isResetting, setIsResetting] = useState(false);
      const [isRobbing, setIsRobbing] = useState(false);
      const [robResult, setRobResult] = useState<string | null>(null);
      const [punchCount, setPunchCount] = useState(0);
      const [playHit] = useSound("/audio/hit_sfx.wav", { volume: 0.1 });
      const [playReset] = useSound("/audio/reset_sfx.wav", { volume: 0.1 });
      const [playRob] = useSound("/audio/rob_sfx.wav", { volume: 0.1 });
    
      const fetchGameRounds = useCallback(() => {
        if (!loaded) return;
        readClownStamina()
          .then((stamina) => {
            setClownStamina(Number(stamina));
          })
          .catch((error) => {
            const message = error instanceof Error ? error.message : String(error);
            console.error("Error fetching clown stamina:", message);
          });
      }, [loaded, readClownStamina]);
    
      // Fetch initial state when contract is loaded
      useEffect(() => {
        fetchGameRounds();
      }, [fetchGameRounds]);
    
      const resetGameState = useCallback(() => {
        setRobResult(null);
        setPunchCount(0);
      }, [punchCount]);
    
      const handleHit = async () => {
        playHit();
        if (!loaded || isHitting) return;
        setIsHitting(true);
        hit()
          .then((hash) => {
            const url = txUrl(hash);
            if (url) {
              notifyInfo(
                React.createElement(ExplorerToast, {
                  url: url,
                  text: "Sent punch tx: ",
                  hash: hash,
                }),
              );
            } else {
              notifyInfo(`Sent punch tx: ${hash}`);
            }
            if (clownStamina && clownStamina > 0) {
              setPunchCount((prev) => {
                const newCount = Math.min(prev + 1, 3);
                return newCount;
              });
            }
            return waitForTransaction(hash);
          })
          .then((receipt) => {
            if (receipt.status === "success") {
              notifySuccess("Punch successful");
              // Re-read stamina from contract after successful hit
              fetchGameRounds();
            } else {
              notifyError("Punch failed");
            }
          })
          .catch((error) => {
            const message = error instanceof Error ? error.message : String(error);
            notifyError(`Error punching clown: ${message}`);
          })
          .finally(() => {
            setIsHitting(false);
          });
      };
    
      const handleReset = async () => {
        playReset();
        if (!loaded || isResetting) return;
        if (clownStamina !== 0) {
          notifyError("Clown must be KO to reset");
          return;
        }
        setIsResetting(true);
        reset()
          .then((hash) => {
            const url = txUrl(hash);
            if (url) {
              notifyInfo(
                React.createElement(ExplorerToast, {
                  url: url,
                  text: "Sent reset tx: ",
                  hash: hash,
                }),
              );
            } else {
              notifyInfo(`Sent reset tx: ${hash}`);
            }
            setPunchCount(0);
            return waitForTransaction(hash);
          })
          .then((receipt) => {
            if (receipt.status === "success") {
              notifySuccess("Reset successful");
              setRobResult(null);
              // Re-read stamina from contract after successful reset
              fetchGameRounds();
            } else {
              notifyError("Reset failed");
            }
          })
          .catch((error) => {
            const message = error instanceof Error ? error.message : String(error);
            notifyError(`Error resetting clown: ${message}`);
          })
          .finally(() => {
            setIsResetting(false);
          });
      };
    
      const handleRob = async () => {
        playRob();
        if (!loaded || isRobbing) return;
        setIsRobbing(true);
        rob()
          .then((result) => {
            setRobResult(result);
          })
          .catch((error) => {
            const message = error instanceof Error ? error.message : String(error);
            notifyError(`Error robbing clown: ${message}`);
          })
          .finally(() => {
            setIsRobbing(false);
          });
      };
    
      return {
        loaded,
        clownStamina,
        currentRoundId,
        isHitting,
        isResetting,
        isRobbing,
        robResult,
        punchCount,
        fetchGameRounds,
        resetGameState,
        handleHit,
        handleReset,
        handleRob,
      };
    };

    Shielded Wallet Client

    Full-featured client with encryption, shielded writes, and signed reads

    Full-featured client for Seismic that handles encryption, shielded writes, and signed reads. Extends viem's wallet client with an ECDH-derived AES encryption pipeline. On construction, it fetches the TEE public key from the node and derives a shared AES key for encrypting calldata.

    hashtag
    Import

    import { createShieldedWalletClient } from "seismic-viem";

    hashtag
    Constructor

    circle-info

    createShieldedWalletClient is async -- it fetches the TEE public key from the node during construction and derives the AES encryption key. Always await the result.

    hashtag
    Parameters

    Parameter
    Type
    Required
    Description

    hashtag
    Return Type

    Promise<ShieldedWalletClient>

    A viem Client extended with PublicActions, WalletActions, EncryptionActions, ShieldedPublicActions, ShieldedWalletActions, DepositContractPublicActions, DepositContractWalletActions, and SRC20WalletActions.

    hashtag
    Usage

    hashtag
    Initialization Lifecycle

    When you call createShieldedWalletClient(), the following steps happen:

    1. Creates a ShieldedPublicClient (or reuses the one provided via publicClient)

    2. Fetches the TEE public key from the node via seismic_getTeePublicKey RPC

    hashtag
    Actions

    hashtag
    Shielded Wallet Actions

    Action
    Description

    hashtag
    Encryption Actions

    Action
    Description

    hashtag
    Inherited Actions

    The wallet client also includes all actions from ShieldedPublicClient:

    • Shielded public actions -- getTeePublicKey(), explorerUrl(), etc.

    • Precompile actions -- rng(), ecdh(), aesGcmEncryption(), aesGcmDecryption(), hdfk()

    circle-info

    The wallet client automatically includes all public client actions -- you can call getBlockNumber(), getBalance(), etc. directly on it.

    hashtag
    Examples

    hashtag
    Shielded Write

    hashtag
    Signed Read

    hashtag
    Reusing a Public Client

    hashtag
    Send + Inspect Write

    hashtag
    Custom Encryption Key

    hashtag
    getEncryption() Standalone Function

    The encryption derivation logic is also available as a standalone function, separate from any client:

    Parameter
    Type
    Required
    Description

    Returns { aesKey: Hex, encryptionPrivateKey: Hex, encryptionPublicKey: Hex }.

    This is useful when you need the encryption key material without constructing a full wallet client -- for example, to manually encrypt or decrypt data outside of the client's lifecycle.

    hashtag
    See Also

    • -- Read-only client without private key

    • -- Validator staking via deposit()

    • -- Watch and decrypt SRC20 token events

    viem transport (e.g., http())

    account

    Account

    Yes

    viem account (from privateKeyToAccount or custom)

    encryptionSk

    Hex

    No

    Custom encryption private key. If not provided, generates a random secp256k1 key

    publicClient

    ShieldedPublicClient

    No

    Reuse an existing ShieldedPublicClient instead of creating a new one

    Generates an ephemeral secp256k1 keypair
    (or uses the provided
    encryptionSk
    )
  • Derives an AES-256 key via ECDH between the client's private key and the TEE's public key

  • Composes all action layers (PublicActions, WalletActions, EncryptionActions, ShieldedPublicActions, ShieldedWalletActions, DepositContractPublicActions, DepositContractWalletActions, SRC20WalletActions) onto a single viem client

  • Send + inspect write -- broadcasts a real shielded tx and returns the plaintext tx, shielded tx, and txHash

    readContract(params)

    Smart read -- inspects ABI for shielded params; uses signed read if shielded, transparent read otherwise

    sreadContract(params)

    Force signed read -- always authenticated eth_call that proves the caller's identity

    treadContract(params)

    Transparent read -- always standard unsigned call. Rejects account (Seismic zeroes out from on transparent eth_call); use sreadContract for sender-aware reads

    signedCall(params)

    Low-level signed eth_call

    sendShieldedTransaction(params)

    Low-level shielded transaction send

    Manual AES-GCM decryption using the derived key

    ,
    secp256k1Signature()
  • SRC20 actions -- watchSRC20Events(), watchSRC20EventsWithKey()

  • Deposit contract actions -- getDepositRoot(), getDepositCount()

  • Standard viem public actions -- getBlockNumber(), getBalance(), getBlock(), etc.

  • Standard viem wallet actions -- sendTransaction(), signMessage(), signTypedData(), etc.

  • Client's encryption private key. If not provided, generates a random one

    Encryption -- Encryption utilities and getEncryption()
  • Chains -- Chain configurations for Seismic networks

  • Precompiles -- Precompile details and parameters

  • Contract Instance -- Working with contract wrappers

  • chain

    Chain

    Yes

    Chain configuration (e.g., seismicTestnet)

    transport

    Transport

    writeContract(params)

    Smart write -- inspects ABI for shielded params; encrypts if shielded, sends transparent otherwise

    swriteContract(params)

    Force shielded write -- always encrypts calldata with the AES key before sending

    twriteContract(params)

    Transparent write -- always sends with plaintext calldata (unencrypted)

    getEncryption()

    Returns the AES encryption key used for shielded operations

    getEncryptionPublicKey()

    Returns the client's encryption public key (the ephemeral secp256k1 public key)

    encrypt(plaintext, metadata)

    Manual AES-GCM encryption using the derived key

    networkPk

    Hex

    Yes

    The TEE's secp256k1 public key

    clientSk

    Hex

    Shielded Public Client
    Deposit Contract
    SRC20 Event Watching

    Yes

    dwriteContract(params)

    decrypt(ciphertext, metadata)

    No

    const walletClient = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    import { createShieldedWalletClient } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    
    const walletClient = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    
    // Smart write -- auto-detects shielded params in the ABI
    // transfer(saddress, suint256) has shielded params → encrypted seismic tx
    const hash = await walletClient.writeContract({
      address: "0xContractAddress",
      abi: contractAbi,
      functionName: "transfer",
      args: ["0xRecipient", 1000n],
    });
    
    // Smart read -- auto-detects shielded params in the ABI
    // balanceOf(saddress) has shielded params → signed read
    const balance = await walletClient.readContract({
      address: "0xContractAddress",
      abi: contractAbi,
      functionName: "balanceOf",
      args: ["0xMyAddress"],
    });
    import { createShieldedWalletClient } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    
    const walletClient = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
    });
    
    // Shielded write: calldata is encrypted before submission
    const hash = await walletClient.writeContract({
      address: "0xContractAddress",
      abi: contractAbi,
      functionName: "transfer",
      args: ["0xRecipient", 1000n],
    });
    
    const receipt = await walletClient.waitForTransactionReceipt({ hash });
    console.log("Transaction confirmed in block:", receipt.blockNumber);
    // Signed read: proves caller identity to the node
    const balance = await walletClient.readContract({
      address: "0xContractAddress",
      abi: contractAbi,
      functionName: "balanceOf",
      args: ["0xMyAddress"],
    });
    
    console.log("Shielded balance:", balance);
    
    // Transparent read: standard unsigned eth_call (no caller proof)
    const totalSupply = await walletClient.treadContract({
      address: "0xContractAddress",
      abi: contractAbi,
      functionName: "totalSupply",
    });
    
    console.log("Total supply:", totalSupply);
    import {
      createShieldedPublicClient,
      createShieldedWalletClient,
    } from "seismic-viem";
    import { seismicTestnet } from "seismic-viem";
    import { http } from "viem";
    import { privateKeyToAccount } from "viem/accounts";
    
    // Create a shared public client
    const publicClient = createShieldedPublicClient({
      chain: seismicTestnet,
      transport: http(),
    });
    
    // Reuse it across multiple wallet clients
    const walletClient1 = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0xPrivateKey1"),
      publicClient,
    });
    
    const walletClient2 = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0xPrivateKey2"),
      publicClient,
    });
    // dwriteContract broadcasts a real shielded tx AND returns the plaintext
    // and shielded tx views for inspection. txHash is a real on-chain hash.
    const debugResult = await walletClient.dwriteContract({
      address: "0xContractAddress",
      abi: contractAbi,
      functionName: "transfer",
      args: ["0xRecipient", 1000n],
    });
    
    console.log("Plaintext tx:", debugResult.plaintextTx);
    console.log("Shielded tx:", debugResult.shieldedTx);
    console.log("Tx hash:", debugResult.txHash);
    const walletClient = await createShieldedWalletClient({
      chain: seismicTestnet,
      transport: http(),
      account: privateKeyToAccount("0x..."),
      encryptionSk: "0xCustomEncryptionPrivateKey",
    });
    
    // Access encryption details
    const aesKey = walletClient.getEncryption();
    const encPubKey = walletClient.getEncryptionPublicKey();
    import { getEncryption } from "seismic-viem";
    
    const encryption = getEncryption(teePublicKey, clientPrivateKey);
    // encryption.aesKey         -- the derived AES-256 key
    // encryption.encryptionPrivateKey  -- the client's secp256k1 private key
    // encryption.encryptionPublicKey   -- the client's secp256k1 public key

    Ch 3: Game UI Components

    In this chapter, you'll build the game interface — the clown sprite with punch animations, action buttons, and the entry screen. Estimated time: ~15 minutes

    hashtag
    ShowClown: Animated clown sprite

    The clown sprite changes appearance based on how many times it's been hit. Create src/components/game/ShowClown.tsx:

    import { motion, useAnimation } from 'framer-motion'
    import { useEffect, useMemo } from 'react'
    
    import { Box } from '@mui/material'
    
    type ClownProps = {
      isKO: boolean
      isShakingAnimation: boolean
      isHittingAnimation: boolean
      punchCount: number
    }
    
    const ShowClown: React.FC<ClownProps> = ({
      isKO,
      isShakingAnimation,
      isHittingAnimation,
      punchCount,
    }) => {
      const controls = useAnimation()
    
      useEffect(() => {
        if (isShakingAnimation) {
          controls.start({
            rotate: [0, -5, 5, -5, 5, 0],
            transition: { duration: 0.5 },
          })
        } else if (isHittingAnimation) {
          controls.start({
            scale: [1, 0.9, 1.1, 1],
            transition: { duration: 0.3 },
          })
        }
      }, [isShakingAnimation, isHittingAnimation, controls])
    
      // Select the appropriate clown image based on punch count and KO state
      // Using useMemo to prevent recalculating on every render
      const clownImage = useMemo(() => {
        // If shaking, show the shaking clown image
        if (isShakingAnimation) {
          return '/clown_shaking.png'
        }
    
        if (isKO) {
          return '/clownko.png'
        }
    
        let imagePath
        switch (punchCount) {
          case 0:
            imagePath = '/clown1.png'
            break
          case 1:
            imagePath = '/clown2.png'
            break
          case 2:
          case 3:
            imagePath = '/clown3.png'
            break
          default:
            imagePath = '/clown1.png'
        }
    
        return imagePath
      }, [isKO, isShakingAnimation, punchCount])
    
      return (
        <Box
          sx={{
            width: { xs: '70%', sm: '70%', md: '80%', lg: '80%', xl: '100%' },
            height: { xs: '100%', sm: '70%', md: '80%', lg: '80%', xl: '100%' },
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
          }}
        >
          <div className="relative">
            <motion.div animate={controls} className="relative">
              <img
                src={clownImage}
                alt="Clown"
                style={{
                  maxWidth: '100%',
                  height: 'auto',
                  objectFit: 'contain',
                }}
              />
            </motion.div>
          </div>
        </Box>
      )
    }
    
    export default ShowClown

    The sprite progression creates visual feedback as the clown takes damage:

    • 0 hits — clown1.png (full health)

    • 1 hit — clown2.png (damaged)

    • 2–3 hits — clown3.png (heavily damaged)

    • KO — clownko.png (knocked out)

    • Shaking — clown_shaking.png (mid-animation)

    Framer Motion's useAnimation hook controls two animations: a shake (rotation) when the clown gets hit, and a scale punch effect for impact feedback.

    hashtag
    ButtonContainer: Action buttons

    The button container renders the game's action buttons — hit, rob, and reset. Create src/components/game/ButtonContainer.tsx:

    The button layout adapts based on game state and screen size:

    • Desktop — rob button on the left, hit/reset on the right

    • Mobile — both buttons side by side below the clown

    • Clown standing — show Hit button (right)

    hashtag
    ClownPuncher: Main game component

    This component ties everything together. Create src/components/game/ClownPuncher.tsx:

    hashtag
    EntryScreen: Wallet connection

    The entry screen prompts the user to connect their wallet before playing. Create src/components/game/EntryScreen.tsx:

    Clicking the logo opens the RainbowKit wallet connection modal. Once authenticated, the onEnter callback fires and the ClownPuncher component takes over.

    hashtag
    WalletConnectButton: Auth context

    Create the auth context and wallet button used throughout the app. Create src/components/chain/WalletConnectButton.tsx:

    hashtag
    Running the frontend

    Start the frontend dev server:

    Make sure sanvil is running and the contract is deployed (see ). Open http://localhost:5173 in your browser, connect your wallet, and start punching the clown!

    hashtag
    Game flow recap

    1. Connect wallet — RainbowKit modal, ShieldedWalletProvider derives shielded keys

    2. Hit the clown — twrite.hit() sends a shielded transaction, stamina decrements

    3. Clown KO — stamina reaches 0, sprite changes to clownko.png

    Congratulations! You've built a complete Seismic dApp — from smart contract to CLI to web frontend.

    Clown KO — show Reset button (right), Rob button now callable (left)

    Rob a secret — read.rob() performs a signed read, secret is decrypted and displayed

  • Reset — twrite.reset() restores stamina and picks a new random secret for the next round

  • Deploying
    import { useState } from 'react'
    
    import { Box, type SxProps, type Theme } from '@mui/material'
    
    type ButtonContainerProps = {
      clownStamina: number | null
      isHitting: boolean
      isResetting: boolean
      isRobbing: boolean
      handleHit: () => void
      handleReset: () => void
      handleRob: () => void
      position?: 'left' | 'right' | 'mobile'
    }
    
    type ActionButtonProps = {
      onClick: () => void
      active: boolean
      src: string
      alt: string
      className: string
      sx?: SxProps<Theme>
    }
    
    const ActionButton = ({
      onClick,
      active,
      src,
      alt,
      className,
      sx,
    }: ActionButtonProps) => (
      <Box
        onClick={onClick}
        component="div"
        sx={{
          cursor: active ? 'default' : 'pointer',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          ...sx,
        }}
      >
        <img
          src={src}
          alt={alt}
          className={className}
          style={{ width: '100%', height: '100%', objectFit: 'contain' }}
        />
      </Box>
    )
    
    export default function ButtonContainer({
      clownStamina,
      isHitting,
      isResetting,
      isRobbing,
      handleHit,
      handleReset,
      handleRob,
      position = 'mobile',
    }: ButtonContainerProps) {
      const [showRobActive, setShowRobActive] = useState(false)
      const [showResetActive, setShowResetActive] = useState(false)
    
      const handleRobClick = () => {
        if (!isRobbing) {
          setShowRobActive(true)
          setTimeout(() => {
            setShowRobActive(false)
            handleRob()
          }, 200)
        }
      }
    
      const handleResetClick = () => {
        if (!isResetting) {
          setShowResetActive(true)
          setTimeout(() => {
            setShowResetActive(false)
            handleReset()
          }, 200)
        }
      }
    
      const isStanding = clownStamina !== null && clownStamina > 0
    
      const robBtn = {
        onClick: handleRobClick,
        active: isRobbing,
        src: showRobActive ? '/rob_active.png' : '/rob_btn.png',
        alt: 'Rob',
        className: 'look-btn',
      }
    
      const hitBtn = {
        onClick: handleHit,
        active: isHitting,
        src: isHitting ? '/punch_active.png' : '/punch_btn.png',
        alt: 'Punch',
        className: 'punch-btn',
      }
    
      const resetBtn = {
        onClick: handleResetClick,
        active: isResetting,
        src: showResetActive ? '/reset_active.png' : '/reset_btn.png',
        alt: 'Reset',
        className: 'reset-btn',
      }
    
      const rightBtn = isStanding ? hitBtn : resetBtn
    
      if (position === 'left') {
        return (
          <Box
            sx={{
              width: { lg: '100%' },
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'flex-end',
              pr: { lg: 4, xl: 6 },
            }}
          >
            <ActionButton
              {...robBtn}
              sx={{ width: '20rem', marginRight: 6, height: '20rem' }}
            />
          </Box>
        )
      }
    
      if (position === 'right') {
        return (
          <Box
            sx={{
              width: { lg: '100%' },
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'flex-start',
              pl: { lg: 4, xl: 6 },
            }}
          >
            <ActionButton
              {...rightBtn}
              sx={
                isStanding
                  ? {
                      marginLeft: { xs: 0, lg: 8 },
                      height: { lg: '18rem' },
                    }
                  : { width: '20rem', marginLeft: '3rem', height: '18rem' }
              }
            />
          </Box>
        )
      }
    
      // Mobile layout — both buttons side by side
      const MOBILE_SIZE = {
        xs: '12rem',
        sm: '20rem',
        md: '20rem',
        lg: '30rem',
        xl: '30rem',
      }
    
      return (
        <Box
          sx={{
            width: '100%',
            height: '100%',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            gap: { xs: 0, sm: 0, md: 0, lg: 70, xl: 70 },
            marginRight: { xs: 0, sm: 4, md: 4, lg: 6, xl: 0 },
            marginLeft: { xs: 0, sm: 4, md: 4, lg: 6, xl: 0 },
          }}
        >
          <ActionButton
            {...robBtn}
            sx={{ height: MOBILE_SIZE, width: MOBILE_SIZE }}
          />
          <ActionButton
            {...rightBtn}
            sx={
              isStanding
                ? {
                    marginRight: { xs: 0, sm: 4, md: 0, lg: 0, xl: 0 },
                    height: {
                      xs: '10rem',
                      sm: '18rem',
                      md: '20rem',
                      lg: '30rem',
                      xl: '30rem',
                    },
                    width: {
                      xs: '12rem',
                      sm: '14rem',
                      md: '28rem',
                      lg: '30rem',
                      xl: '30rem',
                    },
                  }
                : { height: MOBILE_SIZE, width: MOBILE_SIZE }
            }
          />
        </Box>
      )
    }
    'use client'
    
    import { useEffect, useRef, useState } from 'react'
    
    import { useGameActions } from '@/hooks/useGameActions'
    import {
      Backdrop,
      Box,
      CircularProgress,
      Container,
      Fade,
      Typography,
    } from '@mui/material'
    
    import { useAuth } from '../chain/WalletConnectButton'
    import ButtonContainer from './ButtonContainer'
    import EntryScreen from './EntryScreen'
    import ShowClown from './ShowClown'
    
    const ClownPuncher: React.FC = () => {
      const { isAuthenticated } = useAuth()
      const [showGame, setShowGame] = useState(false)
      const [showSecretSplash, setShowSecretSplash] = useState(false)
      const [showRobRefused, setShowRobRefused] = useState(false)
      const prevRoundIdRef = useRef<number | null>(null)
      const {
        loaded,
        currentRoundId,
        clownStamina,
        isHitting,
        isResetting,
        isRobbing,
        robResult,
        punchCount,
        fetchGameRounds,
        resetGameState,
        handleHit,
        handleReset,
        handleRob,
      } = useGameActions()
    
      useEffect(() => {
        // Only fetch data if authenticated and game is shown
        if (isAuthenticated && showGame) {
          fetchGameRounds()
        }
      }, [fetchGameRounds, isAuthenticated, showGame])
    
      useEffect(() => {
        // Only reset game state when first showing the game or when the round actually changes
        if (
          showGame &&
          (prevRoundIdRef.current === null ||
            (currentRoundId !== null && prevRoundIdRef.current !== currentRoundId))
        ) {
          console.log(
            'Round changed from',
            prevRoundIdRef.current,
            'to',
            currentRoundId,
            '- resetting game state'
          )
          resetGameState()
        }
        // Update the ref to the current round ID
        prevRoundIdRef.current = currentRoundId
      }, [currentRoundId, resetGameState, showGame])
    
      // Show splash screen when lookResult changes to a non-null value
      useEffect(() => {
        if (robResult !== null) {
          setShowSecretSplash(true)
        }
      }, [robResult])
    
      // If not showing the game yet, show entry screen
      if (!showGame) {
        return <EntryScreen onEnter={() => setShowGame(true)} />
      }
    
      const onRob = () => {
        if (clownStamina !== null && clownStamina > 0) {
          setShowRobRefused(true)
          return
        }
        handleRob()
      }
    
      const buttonProps = {
        clownStamina,
        isHitting,
        isResetting,
        isRobbing,
        handleHit,
        handleReset,
        handleRob: onRob,
      } as const
    
      return (
        <Container
          sx={{
            height: '100dvh',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'start',
            px: 4,
          }}
        >
          <Box
            sx={{
              mt: { xs: 3, sm: 3, md: 5, lg: 4, xl: 10 },
              height: {
                xs: '30dvh',
              },
              mb: 2,
              display: 'flex',
              justifyContent: 'center',
            }}
          >
            <img
              src="/cblogo.png"
              alt="Clown Beatdown Logo"
              className="clown-logo"
            />
          </Box>
    
          {/* Splash Screen — secret revealed or rob refused */}
          <Backdrop
            sx={{
              color: '#fff',
              zIndex: (theme) => theme.zIndex.drawer + 1,
              backgroundColor: 'rgba(0, 0, 0, 0.85)',
            }}
            open={(showSecretSplash && robResult !== null) || showRobRefused}
            onClick={() => {
              setShowSecretSplash(false)
              setShowRobRefused(false)
            }}
          >
            <Fade in={(showSecretSplash && robResult !== null) || showRobRefused}>
              <Box
                sx={{
                  backgroundColor: 'background.paper',
                  borderRadius: 4,
                  p: 5,
                  textAlign: 'center',
                  maxWidth: '90%',
                  boxShadow: 24,
                }}
              >
                {showRobRefused ? (
                  <>
                    <Typography
                      variant="h4"
                      fontWeight="bold"
                      color="white"
                      gutterBottom
                    >
                      NOT SO FAST!
                    </Typography>
                    <Typography variant="h6" color="white" gutterBottom>
                      The clown isn't giving up that easily.
                    </Typography>
                    <Typography
                      variant="body1"
                      color="text.secondary"
                      sx={{ mt: 2 }}
                    >
                      Knock him out first!
                    </Typography>
                  </>
                ) : (
                  <>
                    <Typography
                      variant="h4"
                      fontWeight="bold"
                      color="white"
                      gutterBottom
                    >
                      SECRET REVEALED!
                    </Typography>
                    <Typography
                      variant="h1"
                      fontWeight="bold"
                      color="white"
                      gutterBottom
                    >
                      {robResult}
                    </Typography>
                  </>
                )}
                <Typography variant="body1" color="text.secondary" sx={{ mt: 2 }}>
                  (Click anywhere to close)
                </Typography>
              </Box>
            </Fade>
          </Backdrop>
    
          {loaded ? (
            <Box
              sx={{
                display: 'flex',
                flexDirection: { xs: 'column', lg: 'row' },
                justifyContent: { lg: 'space-between' },
                alignItems: 'center',
                width: '100%',
                position: 'relative',
                height: { lg: '500px', xl: '600px' },
                my: { xs: 0, md: 5, lg: 0, xl: 1 },
              }}
            >
              {/* Desktop: left buttons */}
              <Box sx={{ display: { xs: 'none', lg: 'flex' } }}>
                <ButtonContainer {...buttonProps} position="left" />
              </Box>
    
              {/* Clown — rendered once, responsive positioning */}
              <Box
                className="clown-container"
                sx={{
                  display: 'flex',
                  justifyContent: 'center',
                  alignItems: 'center',
                  position: { lg: 'absolute' },
                  left: { lg: '50%' },
                  transform: { lg: 'translateX(-50%)' },
                  zIndex: 2,
                  width: { lg: '50%', xl: '40%' },
                  maxHeight: { xs: '35dvh', md: '30dvh', lg: 'none' },
                }}
              >
                <ShowClown
                  isKO={clownStamina === 0}
                  isShakingAnimation={false}
                  isHittingAnimation={isHitting}
                  punchCount={punchCount}
                />
              </Box>
    
              {/* Desktop: right buttons */}
              <Box sx={{ display: { xs: 'none', lg: 'flex' } }}>
                <ButtonContainer {...buttonProps} position="right" />
              </Box>
    
              {/* Mobile: all buttons below clown */}
              <Box
                sx={{
                  display: { xs: 'flex', lg: 'none' },
                  width: '100%',
                  justifyContent: 'center',
                  alignItems: 'center',
                  marginBottom: { xs: 3, md: 5 },
                }}
              >
                <ButtonContainer {...buttonProps} position="mobile" />
              </Box>
            </Box>
          ) : (
            <CircularProgress size={32} />
          )}
        </Container>
      )
    }
    
    export default ClownPuncher
    import React, { useEffect, useState } from 'react'
    
    import { Box, Container } from '@mui/material'
    
    import { useAuth } from '../chain/WalletConnectButton'
    
    type EntryScreenProps = {
      onEnter: () => void
    }
    
    const EntryScreen: React.FC<EntryScreenProps> = ({ onEnter }) => {
      const { isAuthenticated, isLoading, openConnectModal } = useAuth()
      const [isAnimating, setIsAnimating] = useState(false)
    
      // Automatically enter when user becomes authenticated
      useEffect(() => {
        if (isAuthenticated) {
          setIsAnimating(true)
          setTimeout(() => {
            setIsAnimating(false)
            onEnter()
          }, 500)
        }
      }, [isAuthenticated, onEnter])
    
      const handleLogoClick = () => {
        setIsAnimating(true)
    
        // If not authenticated, open wallet connect modal
        if (!isAuthenticated) {
          setTimeout(() => {
            setIsAnimating(false)
            openConnectModal()
          }, 300)
        }
      }
    
      return (
        <Container
          sx={{
            height: '100dvh',
            width: '100dvw',
            display: 'flex',
            flexDirection: 'column',
            alignItems: 'center',
            justifyContent: 'center',
            position: 'relative',
          }}
        >
          <Box
            sx={{
              display: 'flex',
              justifyContent: 'center',
              flexDirection: 'column',
              cursor: 'pointer',
              transform: isAnimating ? 'scale(0.95)' : 'scale(1)',
              transition: 'transform 0.2s ease-in-out',
            }}
            onClick={handleLogoClick}
          >
            <img
              src="/cblogo.png"
              alt="Clown Beatdown Logo"
              style={{ maxWidth: '100%', height: 'auto' }}
              className="clown-image"
            />
            <Box
              sx={{
                mt: 6,
                color: 'black',
                fontSize: '1.25rem',
                textAlign: 'center',
                opacity: 0.8,
                fontFamily: 'monospace',
                border: '1px solid black',
                borderRadius: '10px',
                padding: '10px',
                backgroundColor: 'var(--midColor)',
              }}
            >
              {isLoading ? '...Loading...' : 'CLICK TO CONNECT'}
            </Box>
          </Box>
        </Container>
      )
    }
    
    export default EntryScreen
    import React, { createContext, useContext, useEffect, useState } from 'react'
    import { useAccount } from 'wagmi'
    
    import { ConnectButton } from '@rainbow-me/rainbowkit'
    import { useConnectModal } from '@rainbow-me/rainbowkit'
    
    // Create authentication context
    type AuthContextType = {
      isAuthenticated: boolean
      isLoading: boolean
      openConnectModal: () => void
      accountName?: string
    }
    
    const AuthContext = createContext<AuthContextType>({
      isAuthenticated: false,
      isLoading: true,
      openConnectModal: () => {},
    })
    
    export const useAuth = () => useContext(AuthContext)
    
    // Wallet icon component using SVG for better quality
    const WalletIcon = () => (
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="currentColor"
        className="w-5 h-5"
      >
        <path d="M2.273 5.625A4.483 4.483 0 0 1 5.25 4.5h13.5c1.141 0 2.183.425 2.977 1.125A3 3 0 0 0 18.75 3H5.25a3 3 0 0 0-2.977 2.625ZM2.273 8.625A4.483 4.483 0 0 1 5.25 7.5h13.5c1.141 0 2.183.425 2.977 1.125A3 3 0 0 0 18.75 6H5.25a3 3 0 0 0-2.977 2.625ZM5.25 9a3 3 0 0 0-3 3v6a3 3 0 0 0 3 3h13.5a3 3 0 0 0 3-3v-6a3 3 0 0 0-3-3H15a.75.75 0 0 0-.75.75 2.25 2.25 0 0 1-4.5 0A.75.75 0 0 0 9 9H5.25Z" />
      </svg>
    )
    
    const WalletButton: React.FC<
      React.PropsWithChildren<
        { onClick: () => void } & React.HTMLAttributes<HTMLButtonElement>
      >
    > = ({ children, onClick, ...props }) => {
      return (
        <button onClick={onClick} className="" {...props}>
          {children}
        </button>
      )
    }
    
    export const AuthProvider: React.FC<React.PropsWithChildren> = ({
      children,
    }) => {
      const { openConnectModal } = useConnectModal() || {
        openConnectModal: () => {},
      }
      const { address, isConnecting, isConnected, isDisconnected } = useAccount()
      const [authState, setAuthState] = useState<AuthContextType>({
        isAuthenticated: false,
        isLoading: true,
        openConnectModal: openConnectModal || (() => {}),
      })
    
      useEffect(() => {
        setAuthState({
          isAuthenticated: isConnected,
          isLoading: isConnecting,
          openConnectModal: openConnectModal || (() => {}),
          accountName: address
            ? `${address.slice(0, 6)}...${address.slice(-4)}`
            : undefined,
        })
      }, [isConnected, isConnecting, isDisconnected, address, openConnectModal])
    
      return (
        <AuthContext.Provider value={authState}>{children}</AuthContext.Provider>
      )
    }
    
    const WalletConnectButton = () => {
      return (
        <ConnectButton.Custom>
          {({
            account,
            openConnectModal,
            chain,
            openAccountModal,
            openChainModal,
            mounted,
            authenticationStatus,
          }) => {
            if (!mounted || authenticationStatus === 'loading') {
              return <></>
            }
            if (!account || authenticationStatus === 'unauthenticated') {
              return (
                <WalletButton onClick={openConnectModal}>
                  <span className="md:inline hidden">CONNECT WALLET</span>
                  <span className="md:hidden">
                    <WalletIcon />
                  </span>
                </WalletButton>
              )
            }
            if (chain?.unsupported) {
              return (
                <WalletButton onClick={openChainModal}>
                  <span className="md:inline hidden">Unsupported chain</span>
                  <span className="md:hidden">
                    <WalletIcon />
                  </span>
                </WalletButton>
              )
            }
            return (
              <WalletButton onClick={openAccountModal}>
                <span className="">
                  <WalletIcon />
                </span>
              </WalletButton>
            )
          }}
        </ConnectButton.Custom>
      )
    }
    
    export default WalletConnectButton
    cd packages/web
    bun dev