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...
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...
Loading...
Loading...
Loading...
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?
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.
Several projects have tried to solve this. None of them let you stay in Solidity.
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)
Computationally expensive. Operations on encrypted data are orders of magnitude slower, limiting what you can practically build.
Each of these approaches forces a tradeoff: learn a new language, accept limited functionality, or live with performance constraints.
Seismic solves on-chain privacy with two innovations working together:
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.
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.
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, 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.
Setting up your local machine to develop with Seismic
Before you begin, make sure your machine meets the following requirements:
x86_64 or arm64 architecture
MacOS, Ubuntu, or Windows (other Linux distros may work but are not officially tested)
The local development suite uses sforge as the testing framework, sanvil as the local node, and ssolc as the compiler.
Download and execute the sfoundryup installation script.
Install sforge, sanvil, ssolc. Expect this to take between 5-20 minutes depending on your machine.
(Optional) Remove old build artifacts in existing projects. You can ignore this step if you aren't working with existing foundry projects.
We recommend adding syntax highlighting via the seismic (or seismic 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.
You're two commands away from running an encrypted protocol
You can play around with shielded types using our starter repository. This assumes you went through everything in Installation.
git clone "https://github.com/SeismicSystems/seismic-starter.git"
cd seismic-starter/packages/contracts
sforge test -vvOr if you prefer a more detailed walkthrough of building on Seismic, check out our Tutorials section.
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.
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 bun installed on your machine. If you do not have bun installed, follow the instructions here to install it on your machine.
Navigate to the contracts subdirectory:
cd packages/contractsInitialize a project with sforge:
This command will:
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)
Edit the .gitignore file to be the following:
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.
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
Using stype variables in arrays and maps
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.
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.
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.
Seismic maintains client libraries for three languages.
seismic-viem composes with viem to add Seismic transaction support, encrypted calldata, and signed reads. See the full documentation.
seismic-react composes with wagmi to provide React hooks for shielded reads, writes, and wallet management. See the full documentation.
seismic-web3 composes with web3.py to interact with Seismic nodes from Python. See the full documentation.
seismic-alloy composes with alloy to provide Seismic transaction types and encryption-aware providers. See the full documentation.
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.
curl https://sh.rustup.rs -sSf | shcurl -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 shellsfoundryup
source ~/.zshenv # or ~/.bashrc, depending on your shellsforge clean # run in your project's contract directoryThe Seismic EVM is approximately a superset of the EVM
Transaction construction and serialization identical to Ethereum (with one new transaction type)
Address generation, gas estimation, and signing work the same as Ethereum
Almost all RPC methods are identical to reth
Standard Solidity bytecode will behave identically on Seismic, with minor exceptions (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
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
CLOAD – load shielded data from storage
CSTORE – write shielded data to storage
TIMESTAMP_MS – get the block timestamp in milliseconds. Note that block.timestamp still returns seconds, matching standard Solidity. Use block.timestamp_ms for millisecond precision
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)
All standard Ethereum precompiles are still available. Seismic added 6 new precompiles to our EVM:
RNG 0x64 — securely generate a random number
ECDH 0x65 — Elliptic Curve Diffie-Hellman, for generating a shared secret given a public key and secret key
AES-GCM Encrypt 0x66 — encrypt data with AES-GCM
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
Seismic uses the same staking contract as Ethereum, which is hardcoded into our Genesis block at address 0x00000000219ab540356cbb839cbe05303d7705fa
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
We support almost every RPC endpoint in Reth, and have added a few more of our own. See the full RPC Methods reference for details.
Seismic-specific methods:
seismic_getTeePublicKey — returns the TEE's encryption public key for ECDH key exchange
Modified Ethereum methods:
eth_call — zeroes the from field on unsigned calls; supports "signed reads" via type 0x4A
eth_sendRawTransaction — accepts Seismic transaction type 0x4A with encrypted calldata
eth_getStorageAt — 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
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.
In this section, you'll:
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.
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.
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.
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.
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.
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
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.
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.
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 0x00...0 instead of actual balances and amounts. Everything else — the Solidity syntax, the EVM execution model, the deployment flow — stays exactly the same.
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
Already have Rust and the Seismic tools installed?
You just ran shielded contract tests locally. See the full quickstart for next steps.
Understand why Seismic exists
See how it works under the hood
Set up my dev environment
Run my first shielded contract
Build a complete app step by step
Build a shielded ERC20 token
Learn about shielded types
Integrate a frontend
Deploy to testnet
Understand the transaction lifecycle
Run a node
Our documentation assumes some familiarity with blockchain app development. Before getting started, it'll help if you're comfortable with:
If you're new to blockchain app development or need a refresher, we recommend starting out with the CryptoZombies tutorial.
If you might benefit from direct support from the team, please don't hesitate to reach out to dev@seismic.systems. We pride ourselves in fast response time.
You can also check out our X account for the latest updates, or join our Discord community.
TypeScript client libraries for Seismic
Seismic provides two TypeScript packages:
seismic-react depends on seismic-viem — install both if you're building a React app, or just seismic-viem for everything else.
Use sfoundry to write and test smart contract code locally before deployment
Seismic's development toolkit closely mirrors Foundry (it's a fork!). The mapping is as follows:
You should use the righthand version of all tools when developing for Seismic to get expected behavior. Our documentation assumes familiarity with foundry.
Substitute sforge for forge to execute against Seismic's superset of the EVM. More on this in the next section.
# 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_KEYUse 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.
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.
Clone the repo and start the dev server:
Open the web app and connect MetaMask
Switch to the Seismic testnet (chain ID 5124)
Fill in the token name, symbol, and initial supply
Click Deploy and confirm the transaction in your wallet
The deployed token address and transaction hash appear on success
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.
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.
The server starts on port 3001 and connects to Seismic testnet at https://testnet-2.seismictest.net/rpc.
Returns all tokens ever deployed through the factory, with their metadata.
Note that total_supply is always in base units (not scaled by decimals).
Returns metadata for a single token by address.
If the address is invalid or the RPC call fails, the server returns a JSON error:
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 Signed Reads for how those work.
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.
Navigate to the CLI subdirectory:
# Assuming you are currently in the contracts directory
cd ../cliInitialize a new bun project
bun init -yNow, create an src/ folder and move index.ts there.
Now, edit package.json to be the following:
Edit .gitignore to be:
Your environment is now set!
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.
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 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.
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:
The _randomIndex function generates a pseudo-random index into the secrets array using on-chain randomness sources:
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)
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
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:
chain: a well-defined Chain object
transport: the method of transport of interacting with the chain (http/ws along with the corresponding RPC URL)
privateKey: the private key to create the client for
Once initialized, it can then be used to perform wallet operations or shielded-specific actions.
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:
abi: the ABI of the contract it is interacting with.
address: the address of the deployed contract it is interacting with.
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.
Proxy-based access to dynamically invoke contract methods.
We will extensively use shielded writes (for addSecret) and signed reads (for rob()) in our CLI.
These are guidelines for writing secure Seismic contracts. Following them will help you avoid the most common privacy mistakes.
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.
Your client library (e.g., seismic-viem) handles Seismic transaction construction automatically when you use shielded write functions.
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.
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.
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.
sforge testUse 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.
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).
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.
The s suffix works with all numeric literal forms:
The shielded type is inferred from the assignment target or surrounding expression:
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.
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.
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.
Complete minimal dApp: provider setup, connect wallet, shielded write, signed read
Every example uses the same provider wrapper that combines RainbowKit, wagmi, and Seismic:
Node.js 18+
A WalletConnect project ID
seismic-react and peer dependencies installed
Hooks - Hook API reference
Wallet Guides - RainbowKit, AppKit, and Privy integration
Installation - Full dependency setup
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.
Navigate to the script folder in your project and open the ClownBeatdown.s.sol file located at:
and add the following to it:
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.
In a separate terminal window, run
in order to spin up a local Seismic node.
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.
Now, from packages/contracts, deploy the contract:
The output will show the deployed contract address (e.g. 0x5FbDB2315678afecb367f032d93F642f64180aa3).
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!
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.
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
Native Currency
ETH (18 decimals)
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
Chains Overview - All supported chains
Sanvil - Local development chains
createSeismicDevnet - Custom chain factory
Wallet Guides - RainbowKit setup guides
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.
Chain ID
31337
Name
Sanvil
RPC (HTTP)
http://127.0.0.1:8545
Native Currency
ETH (18 decimals)
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.
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
Provides instant block mining
Chains Overview - All supported chains
Seismic Testnet - Public testnet configuration
createSeismicDevnet - Custom chain factory
Wallet Guides - RainbowKit setup guides
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.solsuint256[] 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
}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// 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// foundry tool -> seismic version of foundry tool
forge -> sforge
anvil -> sanvil
cast -> scastsanvilbun install
bun run dev:webimport { 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(),
},
});cd packages/api
cargo runGET 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: ..."
}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;
}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);// 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 leakedsuint256 x = 42s; // inferred as suint256
suint8 small = 7s; // inferred as suint8
sint256 neg = -1s; // inferred as sint256suint256 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 minussuint8 a = 255s; // suint8
suint32 b = 1_000s; // suint32
sint128 c = -1s; // sint128
suint256 x = 10s;
suint256 y = x + 5s; // 5s inferred as suint256 from contextsuint256 x = 1s + 1; // Error — mixed shielded/non-shielded
suint256 x = 1s + 1s; // OKuint256 x = 42s; // Error — cannot assign shielded to uint256import { 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-querypackages/contracts/script// 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));
}
}sanvilRPC_URL=http://127.0.0.1:8545
PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80source .env
sforge script script/ClownBeatdown.s.sol:ClownBeatdownScript \
--rpc-url $RPC_URL \
--broadcastscast 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 $PRIVKEYimport { 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(),
},
});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
sfoundryupsanvilSeismic 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.
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
The Seismic compiler (ssolc) is a fork of solc that understands shielded types. When you write:
suint256 balance = 100s;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
sbytes -- shielded bytes, both fixed-length (sbytes1 through sbytes32) and dynamic (sbytes)
saddress -- shielded address
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.
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 Summit (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
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:
The client calls seismic_getTeePublicKey on the RPC to fetch the network's TEE public key.
The client performs ECDH key agreement between the user's private key and the TEE public key to derive a shared secret.
The calldata is encrypted using AEAD (authenticated encryption with associated data).
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.
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 The Seismic Transaction.
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:
(value, is_private)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 Seismic EVM.
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.
Here is what happens when a user calls transfer() on an SRC20 contract with shielded balances:
mapping(address => suint256) balanceOf;
function transfer(address to, suint256 amount) public {
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
}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.
Seismic adds six precompiled contracts to the EVM, giving smart contracts access to cryptographic primitives that would be prohibitively expensive to implement in Solidity:
0x64
Securely generate a random number
0x65
These precompiles enable contracts to perform on-chain encryption, key derivation, and random number generation without relying on external oracles or off-chain computation.
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:
Transaction processing, state management, RPC
Consensus and block production
Enclave
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.
The Seismic node is a fork of reth (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 precompiles. This is built on a forked version of revm.
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.
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.
The Seismic execution stack is built on a chain of forks from the Ethereum Rust ecosystem:
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-dbseismic-alloy-core (fork of alloy-core): Shielded types and FlaggedStorage primitives.
seismic-trie (fork of alloy-trie): Merkle trie encoding for FlaggedStorage values.
seismic-revm (fork of revm): CLOAD/CSTORE opcodes, Seismic precompiles, and FlaggedStorage access rules.
(fork of revm-inspectors): EVM tracing/debugging with Seismic support.
(fork of alloy-evm): Seismic block execution layer.
(fork of reth): Full node with Enclave communication, modified RPC (redacting shielded data), and TEE attestation.
(fork of solidity): Compiler adding shielded types (suint, sbool, sbytes, saddress).
(fork of foundry): sforge, sanvil, scast dev tools.
(fork of compilers): Compiler integration for sforge.
(fork of foundry-fork-db): Fork DB with FlaggedStorage support.
Plus two original repos:
summit: Consensus client.
seismic-enclave: ECDH + AES-GCM crypto for transaction encryption/decryption.
seismic-alloy: Rust SDK with TxSeismic type and encryption-aware providers.
For the full list and dependency flow, see Repos.
Summit is Seismic's consensus layer, built on Commonware 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.
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:
A new node generates an attestation report inside its TEE.
The report is sent to existing nodes for verification.
Existing nodes check that the report was generated by genuine Intel TDX hardware and that the code measurement matches the approved build.
Only after successful verification does the new node receive the root key.
This ensures that every node in the network is running the same software and that no node can be modified to leak private data.
The TEE (Trusted Execution Environment) is the foundation of Seismic's privacy model. Intel TDX provides the following guarantees:
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.
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.
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.
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.
Low-entropy private storage values: Shielded storage hides values from RPC reads, but each value is still committed into the storage Merkle trie at a publicly known slot. A value with little entropy — a flag, a small enum — can be recovered by guessing candidates and checking them against the public trie commitment, without ever reading the slot. See for details and mitigations.
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.
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 SRC20 tutorial.
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.
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;
}
}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.
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.
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.
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.
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.
Deploys a new SRC20 token through the factory and returns the deployed address.
async function createToken(
client: ShieldedWalletClient,
params: CreateTokenParams,
): Promise<CreateTokenResult>;Reads public metadata from a deployed SRC20 token using a standard public client.
Resolves the factory contract address for a given chain ID. Throws if the chain is not supported.
The raw mapping of chain IDs to factory addresses:
The package exports the full contract ABIs:
SRC20FactoryAbi — createToken, getTokenCount, tokens(uint256), TokenCreated event
SRC20TokenAbi — name, symbol
\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.
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
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.
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.
Basic understanding of ERC20 -- You should know what balanceOf
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.
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.
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.
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.
A counter that increments with each new round/reset, ensuring every round has a fresh clown to beat down.
A mapping that records every player's contribution to the current round, ensuring only participants can rob the clown's 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 using _randomIndex()
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]) by 1
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 sbytes to
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
Modifiers enforce the rules of the game:
Ensures that rob() and reset() can only be called if the clown's stamina has reached zero.
Ensures that hit() can only be called while the clown still has stamina remaining.
Restricts access to rob(), and hence the secret being revealed, only to players who contributed at least one hit in the current round.
Create the project folder and navigate into it:
mkdir clown-beatdown
cd clown-beatdownCreate the packages directory with subdirectories for contracts and cli
mkdir -p packages/contracts packages/cliThe 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.
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.
Replace the default package.json with the following content for a monorepo setup:
Add the following to the .prettierrc file for consistent code formatting:
Replace the .gitignore file with:
Add the following to the .gitmodules file to track git submodules (in our case, only the Forge standard library, forge-std):
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/libconstants.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.
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.
All comparisons and operators for shielded integers are functionally identical to their unshielded counterparts.
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)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.
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.
Fixed-size shielded bytes mirror the standard bytes1–bytes32 types.
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.
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.
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.
dApps wanting polished wallet UI
Regardless of which wallet library you choose, the integration follows the same steps:
Install the wallet library alongside seismic-react
Configure wagmi with Seismic chain definitions
Nest providers in the correct order
Use hooks from
All wallet integrations require the same provider hierarchy:
All wallet libraries use the same chain imports from seismic-react/rainbowkit:
Want the easiest setup with a beautiful wallet modal? Use
Need email/social login or embedded wallets? Use
Want WalletConnect with maximum wallet compatibility? Use
-- Package setup and peer dependencies
-- Provider reference and configuration
-- All available hooks
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.
import { createSeismicDevnet } from "seismic-react/rainbowkit";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)
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.
- All supported chains
- Public testnet configuration
- Local development chains
- RainbowKit setup guides
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.
That's it. The CLI walks you through the rest interactively, or you can pass everything up front:
Example output:
Solidity interface reference for SRC20Factory and SRC20Token
Pre-deployed on Seismic testnet at 0x87F850cbC2cFfac086F20d0d7307E12d06fA2127.
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
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.
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:
Shielded types cannot be
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:
Start by importing all the necessary modules and functions at the top of app.ts:
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.
The App
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.
This applies to all shielded types: suint, sint, sbool, saddress, and sbytes.
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.
If you need the value to be confidential from the start, it should arrive as encrypted calldata via a Seismic transaction (type 0x4A
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.
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.
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.).
Emits the Hit event to update all participants.
bytesIncrements the round counter
Emits the Reset event.
Elliptic Curve Diffie-Hellman -- derive a shared secret from a public key and a private key
0x66
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
TEE operations: key management, encryption/decryption, attestation



decimalsownertotalSupplymintburntransfertransferFromapprovebalanceallowancebalanceOfSignedpermitclient
ShieldedWalletClient
yes
A shielded wallet client from seismic-viem
params.name
string
yes
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
bun init -y && rm index.ts && rm tsconfig.json && touch .prettierrc && touch .gitmodulesimport { join } from "path";
const CONTRACT_NAME = "ClownBeatdown";
const CONTRACT_DIR = join(__dirname, "../../contracts");
export { CONTRACT_NAME, CONTRACT_DIR };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 Footguns for more detail.
seismic-reactBuilt-in modal, chain switching, account display
Apps needing email/social login
Embedded wallets, onboarding flow
WalletConnect ecosystem apps
WalletConnect modal, broad wallet support
Setup with RainbowKit wallet UI
Embedded wallets with Privy
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.
WalletConnect AppKit integration
https://<nodeHost>/rpcRPC (WSS): wss://<nodeHost>/ws
Seismic transaction formatters
nodeHost
string
Yes
Hostname for the node (e.g. testnet-1.seismictest.net)
explorerUrl
string
No
Block explorer URL
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.
These helper methods ensure that the app fetches the correct contract instance for a specific player, supporting multiplayer scenarios.
getPlayerContract:
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.
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);
}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",
};{
"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-stdimport 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 };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 firstsbytes32 a = sbytes32(0xabcd);
sbytes1 b = sbytes1(0xff);suint256 a = suint256(42); // explicit cast
suint256 b = 42s; // s suffix — same resultWagmiProvider
└─ QueryClientProvider
└─ [Wallet Provider] (RainbowKit / Privy / AppKit)
└─ ShieldedWalletProvider
└─ Your Appimport { seismicTestnet } 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/wsimport { 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",
});# Assuming you are in packages/cli/lib
cd ../src
touch app.tsimport {
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)
}transferapprovetransferFrom3
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
Shielded types and the minimal diff
2
suint256 in practice, testing with sforge
--name
Token name
prompted
--symbol
Token symbol
prompted
--supply
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
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.
The factory exposes four interfaces depending on how you want to integrate:
Deploy a token manually from the terminal
Embed token creation in a dApp or script
Chain ID: 5124 (Seismic testnet)
Factory address: 0x87F850cbC2cFfac086F20d0d7307E12d06fA2127
Explorer: seismic-testnet.socialscan.io
bunx create-src20bunx create-src20 \
--name "My Private Token" \
--symbol "MPT" \
--supply 1000000 \
--key 0xYourPrivateKey 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...The public tokens array stores every token address in deployment order. Access by index:
Returns tokens.length. Use this to iterate the full list.
Emitted on every successful deployment. The name and symbol fields are unencrypted strings; creator and token are indexed for log filtering.
Each token deployed by the factory is an instance of SRC20Token, which extends the base SRC20 contract from seismic-std-lib.
Only callable by owner. Both revert with "NOT_OWNER" if called by anyone else. The amount parameter is suint256 — it is shielded in transit.
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
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 Signed Reads for details.
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.
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);
}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
}suint256Vanilla eth_call has no sender identity. On Seismic, the from field is zeroed out for unsigned eth_call 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.
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.
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 flag in SeismicElements should be set to true, in order to prevent anyone from replaying this payload as a write transaction.
From the contract's perspective, a signed read looks like a normal view function call. The difference is entirely at the transport layer.
Add a balanceOf function that requires the caller to be the account owner:
This function does three things:
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.
Casts to 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.
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.
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.
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.
You can also call readContract on the wallet client:
Both approaches produce the same signed read.
The signed read has several layers of protection:
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 result is that only the account owner can view their balance, and the balance is never exposed in plaintext outside the TEE.
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, Intelligence Contracts, we will add authorized roles (such as compliance officers) who can view specific balances when required -- without compromising privacy for everyone else.
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],
});When you cast from a shielded type to an unshielded type, you are "unshielding" the value -- making it publicly visible.
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.
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:
You can also convert in the other direction:
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:
Since shielded types cannot be returned from public or external functions, you must unshield them first:
When calling a contract that expects unshielded types, cast at the call boundary:
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:
Every cast between shielded and unshielded types is a potential information leak point. Keep these principles in mind:
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.
Shielded-to-unshielded casts expose the output value in the trace. Only unshield when you intend the value to become public.
Minimize casts. The fewer times you cross the shielded/unshielded boundary, the smaller your attack surface.
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.
uint256 publicNumber = 100;
// Implicit casting -- will NOT compile
suint256 shielded = publicNumber; // Error
// Explicit casting -- correct
suint256 shielded = suint256(publicNumber); // OKbool 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); // OKuint256 publicValue = 100;
suint256 shieldedValue = suint256(publicValue);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.
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 truncatesuint256 unsigned = suint256(100);
sint256 signed = sint256(unsigned); // Reinterprets the bitssuint256 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.
}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.
Privacy consideration: Unshielding to address payable exposes the address in the transaction trace.
createShieldedWalletClient / createShieldedPublicClient; you can also extend a vanilla viem client with them manually.An account registers its AES viewing key with the Directory contract.
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
The watcher subscribes with a filter on encryptKeyHash so it only receives events it can decrypt.
For each matching log, the client decrypts encryptedAmount with the AES key and invokes the user callback with a DecryptedTransferLog / DecryptedApprovalLog.
Watches events for the connected wallet. The viewing key is fetched automatically from the Directory contract via a signed read.
address
Address
Yes
SRC20 token contract address
Returns: Promise<() => void> -- call the returned function to stop watching.
Same filter + decryption flow, but takes an explicit viewing key instead of fetching it from the Directory.
viewingKey
Hex
Yes
32-byte AES key used to decrypt amounts
Returns: Promise<() => void> -- call the returned function to stop watching.
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;
};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.
useShieldedContract
useContract
Get a ShieldedContract instance
useShieldedWriteContract
useWriteContract
Send encrypted write transactions
useSignedReadContract
useReadContract
Execute authenticated read calls
Always verify the wallet is loaded before calling contract methods:
The write and read hooks expose isLoading so you can disable buttons or show spinners:
Every hook returns an error field. Check it after operations complete:
Access shielded wallet and public clients from context
Get a ShieldedContract instance for reads and writes
ShieldedWalletProvider -- Context provider required by all hooks
Installation -- Package setup and peer dependencies
Seismic React Overview -- SDK architecture and quick start
useShieldedWallet
useAccount + useWalletClient
Access shielded public and wallet clients
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);
}
}seismicTestnet
5124
Public testnet
sanvil
31337
Local Sanvil dev node
localSeismicDevnet
5124
Local seismic-reth --dev
createSeismicDevnet()
5124
Factory for custom chain configs
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.
Public testnet configuration and usage
Local development chains (Sanvil + seismic-reth)
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";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()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.
Navigate to the test folder in your project and open the ClownBeatdown.t.sol file located at:
packages/contracts/test/ClownBeatdown.t.solThis 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.
Start off with testing the basic functionalities: hit, rob, and reset.
Basic hit functionality
Ensures the clown's stamina decreases when hit.
Knockout and rob
Validates that after knocking out the clown, a contributor can rob a valid secret.
Reset functionality
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.
Preventing hit when clown is down
Ensures that hitting a knocked-out clown is not allowed.
Preventing rob when clown is standing
Ensures that robbing while the clown still has stamina is not allowed.
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.
Prevent Non-Contributors From Using rob()
Ensures that only contributors in the current round can call rob().
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!
Install seismic-viem and configure viem peer dependency
Node.js
18+
LTS recommended; install via or
Install seismic-viem alongside its viem peer dependency using your preferred package manager:
seismic-viem has a single peer dependency:
The following cryptographic libraries are bundled internally and do not need to be installed separately:
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.
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.
seismic-viem is written in TypeScript and ships its own type declarations. No additional configuration is required beyond a standard tsconfig.json:
If you see a peer dependency warning for viem, ensure you have viem 2.x installed:
seismic-viem requires Node.js 18 or newer for native fetch and crypto support:
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.
-- Configure network connections
-- Full SDK overview and architecture
-- React hooks layer built on seismic-viem
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()
depositContractWalletActions -- write: deposit()
Both extensions are already applied by createShieldedPublicClient and createShieldedWalletClient; you can also extend a vanilla viem client with them manually.
DEPOSIT_CONTRACT_ADDRESS is 0x00000000219ab540356cBB839Cbe05303d7705Fa. Every action accepts an optional address to override this default.
Returns the current deposit merkle root.
Returns: Promise<Hex> -- the SHA-256 deposit root.
Returns the total number of deposits accepted by the contract.
Returns: Promise<Hex> -- the deposit count encoded as a little-endian 64-bit number.
Registers a validator by submitting a deposit transaction.
Returns: Promise<Hex> -- the transaction hash.
-- base client that already includes these public actions
-- base client that already includes these wallet actions
Execute authenticated read calls on shielded contracts
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";-- Send encrypted write transactions
-- Contract instance with both read and write methods
-- Access the underlying wallet client
-- Context provider required by this hook
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.
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, visible to anyone who queries the contract or reads the chain.
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.
Two changes here. First, the value type changes from uint256 to suint256. This tells the Seismic compiler to emit
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.
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 (value, is_private). When the compiler sees suint256, it emits CSTORE to write and CLOAD to read, setting the is_private flag to true
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.
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:
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:
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.
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.
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
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.
Create a .env in packages/cli:
Open .env and paste the following:
What's Happening Here?
Shielded types cannot be emitted directly in events. The following will not compile:
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.
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.
Install seismic-react and configure peer dependencies
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 , built on 2.0+ and 2.x. Provides ShieldedWalletProvider context and hooks for encrypted transactions and signed reads in React applications.
Minimal setup with RainbowKit:
The SDK wraps wagmi's connector layer to inject Seismic's shielded wallet and public clients:
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.
Define the Seismic chain configuration for Privy:
Create a wagmi config using Privy's wagmi integration:
Nest the providers in the correct order -- Privy wraps wagmi, and ShieldedWalletProvider goes inside:
Use Privy's hooks to trigger the login modal:
Once authenticated, use seismic-react hooks as normal:
Instantiating contracts and interacting through shielded and transparent namespaces
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.
ShieldedContract gives you five namespaces:
Sync contract wrapper with transparent read-only access
Synchronous contract wrapper for read-only transparent access to Seismic contracts.
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.
Set encrypted allowance
balance() → uint256
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
The signed_read flag is set to true. If someone intercepts the signed read payload and submits it to eth_sendRawTransaction, it is rejected.
No state changes
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.
onTransfer
(log: DecryptedTransferLog) => void
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
params
WatchSRC20EventsParams
Yes
Same shape as watchSRC20Events above
Deploy via browser without writing code
Query deployed tokens from any language
Send encrypted write transactions
Execute authenticated read calls
Factory for custom chain configurations
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
npm install seismic-viem viemyarn add seismic-viem viempnpm add seismic-viem viembun add seismic-viem viemAES-GCM encryption and decryption
Hex
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)
address
Address
No
Deposit contract address (defaults to the canonical)
address
Address
No
Deposit contract address (defaults to the canonical)
nodePubkey
Hex
Yes
Validator ED25519 public key
consensusPubkey
isLoading
boolean
Whether a read is in progress
error
Error | null
Error from the most recent read
Hooks Overview -- Summary of all hooks
address
`0x${string}`
Yes
Contract address
abi
Abi
Yes
Contract ABI
functionName
string
Yes
Name of the view/pure function to call
args
array
No
Arguments to pass to the function
signedRead
() => Promise<any>
Function to execute the signed read
read
() => Promise<any>
Alias for signedRead
ShieldedWalletProvider -- Provider reference and options
useShieldedWallet -- Access shielded wallet context
RainbowKit Guide -- Traditional wallet modal alternative
AppKit Guide -- WalletConnect modal alternative
CSTORECLOADSSTORESLOADpublicpublicThe same change applies to the allowance mapping. The nested mapping's value type becomes suint256, and the public modifier is removed.
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.
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.
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.
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.
There are two capabilities that the basic SRC20 loses compared to a standard ERC20:
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.
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.
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.
Here is the full transfer implementation with shielded amounts:
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.
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.
Storage update -- The new balances are written via CSTORE. Both the sender's and recipient's balance slots have is_private = true.
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.
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.
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.
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.
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.
Create a test file at test/SRC20.t.sol:
To support this test, add a test-only helper to your contract:
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 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.
Add a public view function so the current stamina can be checked:
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:
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".
Round tracking, so each reset creates a new round.
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.
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().
Round Tracking: The round counter increments each time the clown is reset, allowing us to distinguish between rounds.
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.
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!
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)
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:
Import the required libraries to read environment variables, define network configurations, and interact with the ClownBeatdown contract.
This function initializes the contract and player wallets, then runs the game session.
The contract's ABI and deployed address are read from files generated during deployment.
Determine whether to use the local sanvil node (31337) or the Seismic testnet.
Assign Alice and Bob as players with private keys stored in .env.
Create an App instance to interact with the ClownBeatdown contract.
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)
This ensures that the script runs when executed.
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// 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 viemimport { 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 viem@2.x.xnode --version
# Should be >= v18.0.0import {
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"),
});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>
)
}npm install @privy-io/react-auth @privy-io/wagmi wagmi viem @tanstack/react-query seismic-react seismic-viemimport { 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>
)
}// 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");
_;
}
} // 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");
_;
}
}touch .envCHAIN_ID=31337
VITE_CHAIN_ID=31337
RPC_URL=http://127.0.0.1:8545
ALICE_PRIVKEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
BOB_PRIVKEY=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690dimport 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 revertGenerate a shared secret between the sender and the intended recipient using the ECDH precompile at address 0x65.
Derive an encryption key from the shared secret using the HKDF precompile at address 0x68.
Encrypt the event data using the AES-GCM Encrypt precompile at address 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 at address 0x67, either off-chain or on-chain if needed.
ECDH
Shared secret generation
AES-GCM Encrypt
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 SRC20: Private Token tutorial.
The built-in helpers ecdh(), hkdf(), and aes_gcm_encrypt() are compiler-provided globals — no imports needed. See the Precompiles reference for details on each.
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:
Python — watch_src20_events_with_key and SRC20EventWatcher
TypeScript (viem) — watchSRC20EventsWithKey() action
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.
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.
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.
event ConfidentialEvent(suint256 confidentialData); // Compilation error// 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!
}^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
seismic-react requires several peer dependencies that your project must provide:
react
React runtime
wagmi
Ethereum React hooks and wallet connectors
viem
Install all required peer dependencies:
Create a wagmi config with Seismic chain definitions:
seismic-react provides two entry points:
seismic-react
Main entry -- ShieldedWalletProvider, useShieldedWallet, useShieldedContract, useShieldedWriteContract, useSignedReadContract
seismic-react/rainbowkit
Chain configs -- seismicTestnet, sanvil, localSeismicDevnet, createSeismicDevnet
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.
Ensure seismic-viem is installed as a peer dependency:
seismic-react requires wagmi v2. If you have wagmi v1 installed, upgrade:
Make sure you import chain configs from the seismic-react/rainbowkit entry point, not from the main entry:
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:
ShieldedWalletProvider -- Provider setup and configuration
Hooks Overview -- Available React hooks
RainbowKit Guide -- Wallet UI integration
Privy Guide -- Embedded wallet setup
Node.js
18+
LTS recommended
React
npm install seismic-reactyarn add seismic-reactpnpm add seismic-reactbun add seismic-reactnpm install react wagmi viem seismic-viem @tanstack/react-queryimport { 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-viemnpm 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"
}
}Replace YOUR_WALLETCONNECT_PROJECT_ID with a project ID from .
Package setup, peer dependencies, and configuration
React context provider for shielded clients
Summary of all available hooks
Access shielded wallet and public clients
Connecting different wallet providers
Setup with RainbowKit wallet UI
Connect a shielded wallet -> ShieldedWalletProvider
Read shielded state -> useSignedReadContract
Send an encrypted transaction -> useShieldedWriteContract
Get a contract instance -> useShieldedContract
Install the package ->
Set up RainbowKit ->
Provider -> ShieldedWalletProvider
Chain configs -> seismicTestnet, sanvil, localSeismicDevnet, createSeismicDevnet
Shielded Transactions -- Encrypt calldata before sending via useShieldedWriteContract
Signed Reads -- Prove caller identity in eth_call with useSignedReadContract
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
Install seismic-react -- Add the package and peer dependencies
Set up ShieldedWalletProvider -- Wrap your app with the provider
Connect a wallet -- Choose a wallet integration
Use hooks -- Read and write shielded contract state
npm install seismic-reactimport { 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>
)
}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.write
Encrypted transaction (TxSeismic type 0x4a)
Calldata encrypted
.read
Encrypted signed eth_call
Calldata + result encrypted
.twrite
Standard eth_sendTransaction
Calldata plaintext
Write namespaces accept optional keyword arguments for transaction parameters:
All code snippets in the Python SDK docs reference the interface below.
Token examples use the real SRC20 / ERC20 specs (balanceOf, transfer, approve, allowance, totalSupply, etc.).
If you need to encode calldata outside of a contract call — for example, to pass it to the low-level API — you can use encode_shielded_calldata. This computes the function selector using the original shielded type names (like suint256) but encodes the parameters using standard types (like uint256):
contract = w3.seismic.contract(address="0x...", abi=ABI)# 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 hashtx_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])w3
Web3
Yes
Synchronous Web3 instance connected to RPC endpoint
address
ChecksumAddress
Yes
Contract address (checksummed Ethereum address)
abi
list[dict[str, Any]]
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)
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
AsyncPublicContract - Async version of this class
ShieldedContract - Full contract wrapper with write operations
create_public_client - Create client without private key
Contract Instance Guide - 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}")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 wagmi to provide shielded reads, shielded writes, and encrypted communication out of the box. Estimated time: ~25 minutes.
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.
Install the required packages:
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.
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.
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.
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:
Fetches the TEE public key from the node.
Derives a shared secret via ECDH.
Encrypts the calldata (including the recipient and amount) with AEAD.
Wraps everything in a Seismic transaction (type 0x4A).
The user sees a standard wallet confirmation prompt. The privacy happens automatically under the hood.
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.
Here is the full dashboard component that ties everything together:
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.
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.
The FlaggedStorage model enforces strict separation between public and confidential data:
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.
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).
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.
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.
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.
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.
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.
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.
signedReadContract() -- standalone function, same API shape as viem's readContract
The signed read paths all encrypt the calldata, sign it, and decrypt the response automatically.
signedReadContract(client, parameters, securityParams?)
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.
Promise<ReadContractReturnType> -- the decoded return value, typed according to the ABI.
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.
When you call signedReadContract (or contract.sread.functionName), the SDK performs the following steps:
ABI-encode the function call into plaintext calldata
Build Seismic metadata with signedRead: true
Encrypt calldata with AES-GCM using the shared key derived via ECDH
Sign the transaction:
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.
-- getShieldedContract with .read and .tread namespaces
-- Encrypted write transactions using the same pipeline
-- ECDH key exchange and AES-GCM details
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.
Promise<Hash> -- the transaction hash.
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.
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:
When you call shieldedWriteContract (or contract.swrite.functionName), the SDK performs the following steps:
ABI-encode the function call into plaintext calldata
Build Seismic metadata -- encryption nonce, recent block hash, expiry block
Encrypt calldata with AES-GCM using a shared key derived via ECDH between your ephemeral keypair and the node's TEE public key
Construct a type
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.
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:
-- getShieldedContract with .write and .dwrite namespaces
-- Authenticated reads that also use the encryption pipeline
-- ECDH key exchange and AES-GCM details
Send encrypted write transactions to shielded contracts
Hook for sending shielded write transactions. Encrypts calldata before submission so transaction data is not visible on-chain.
import { useShieldedWriteContract } from "seismic-react";address
Hex
The hash field updates after each successful write. Use it to link to a block explorer or track confirmation.
Pass gas or gasPrice to override the automatic estimates:
-- Perform signed, encrypted read calls
-- Contract instance with both read and write methods
-- Access the underlying wallet client
-- Context provider required by this hook
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.
npm install @rainbow-me/rainbowkit wagmi viem @tanstack/react-query seismic-react seismic-viemCreate a wagmi config using RainbowKit's getDefaultConfig with Seismic chains:
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],
});Replace YOUR_WALLETCONNECT_PROJECT_ID with a project ID from .
Wrap your app with the provider stack. The nesting order matters:
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>
)
}RainbowKit provides a pre-built connect button component:
import { ConnectButton } from '@rainbow-me/rainbowkit'
function Header() {
return (
<header>
<ConnectButton />
</header>
)
}Once the providers are in place, use seismic-react hooks to interact with shielded contracts:
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>
}Next.js App Router requires client components for providers. Create a separate providers file:
// 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>
)
}Then wrap your root layout:
// 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>
)
}For local development, use the sanvil chain (Seismic's local development node) instead of seismicTestnet:
A full working setup in a single file:
-- Comparison of wallet libraries
-- Provider reference and options
-- Access shielded wallet context
-- Email/social login alternative
Async contract wrapper with transparent read-only access
Asynchronous contract wrapper for read-only transparent access to Seismic contracts.
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 operations. All methods return coroutines that must be awaited. Use this class in async applications when you only need to read public contract data.
class AsyncPublicContract:
def __init__(
self,
w3: AsyncWeb3,
address: ChecksumAddress,
abi: list[dict[str, Any]],
) -> None:
...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)
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
- Synchronous version of this class
- Full async contract wrapper with write operations
- Create async client without private key
- Overview of contract interaction patterns
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.
import { useShieldedWallet } from "seismic-react";publicClient
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.
If initialization fails (for example, the node is unreachable or the wallet connector is incompatible), the error field contains the error message.
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.
-- Context provider that supplies the shielded clients
-- Create a contract instance using the wallet client
-- Send encrypted write transactions
-- Perform signed, encrypted read calls
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.
Events in Ethereum (and Seismic) are stored in transaction logs, which are public. You cannot use shielded types directly in event parameters:
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.
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
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.
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.
Real-world token issuers need to answer questions like:
Can a compliance officer verify that an account's balance is below a threshold?
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
From the root of your clown-beatdown monorepo, create a new Vite + React + TypeScript project:
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:
Copy the public/ folder from the repo into packages/web/public/. This includes the clown sprites, button images, background, logo, and audio files used by the game UI.
Update vite.config.ts
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.
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 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.
The problem: Assigning literal values to shielded types — whether via explicit cast or the
TypeScript client library for Seismic, composing with viem to add shielded transactions, encrypted calldata, and signed reads.
Low-level TypeScript SDK for , built on 2.x. Provides shielded public and wallet clients, encrypted contract interactions, signed reads, chain configs, and precompile access. This is the foundation that builds upon.
Python SDK for Seismic, built on web3.py
Python SDK for , built on . Requires Python 3.10+.
Or with :
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.
Create a wagmi adapter and initialize AppKit with Seismic chains:
Nest the providers with ShieldedWalletProvider inside:
AppKit provides a web component for the connect button:
Once connected, use seismic-react hooks to interact with shielded contracts:
-- Comparison of wallet libraries
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.
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.
Provider setup with RainbowKit + Seismic
Wallet connection via RainbowKit's ConnectButton
.tread
Standard eth_call
Calldata plaintext
.dwrite
Debug write — like .write but returns plaintext + encrypted views
Calldata encrypted
Yes
Contract ABI (list of function entries)
Encrypt data with AES-GCM
AES-GCM Decrypt
Decrypt data with AES-GCM
HKDF
Key derivation from shared secret
Create a contract instance with ABI binding
Send encrypted write transactions
Perform signed, encrypted read calls
Embedded wallets with Privy
WalletConnect AppKit integration
Broadcasts the encrypted transaction.
Extend the contract -- Consider adding features like burn functions or governance mechanisms.
CLOAD on a public slot
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
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 Be Mindful of Gas-Based Information Leakage for details.
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.
Returns the value
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.
Abi
Yes
Contract ABI
functionName
string
Yes
Name of the view/pure function
args
array
No
Function arguments
nonce
number
No
Override the nonce
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
Signer if shielded, zero if not
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
Aspect
.read (Smart)
.sread (Force Signed)
.tread (Transparent)
Calldata
Auto-detected by ABI
Always encrypted
Always plaintext
.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.
abi
msg.sender
Abi
Yes
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
Sign and broadcast the transaction
Hex
Random
Override the encryption nonce
recentBlockHash
Hex
Latest
Override the recent block hash
expiresAtBlock
bigint
Calculated
Override the expiry block directly
Shielded Wallet Client -- Creating the client used for shielded writes
address
Hex
Yes
Contract address
blocksWindow
bigint
100n
Number of blocks before the transaction expires
abi
encryptionNonce
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
Hooks Overview -- Summary of all hooks
Yes
Contract address
abi
Abi
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
writeContract
() => Promise<Hex>
Function to execute the shielded write
write
() => Promise<Hex>
Alias for writeContract
AppKit Guide -- WalletConnect modal alternative
Contract ABI (list of function entries)
No authentication: Standard unsigned async eth_call operations
Gas not consumed: eth_call is free (doesn't create transactions)
w3
AsyncWeb3
Yes
Asynchronous AsyncWeb3 instance connected to RPC endpoint
address
ChecksumAddress
Yes
Contract address (checksummed Ethereum address)
abi
list[dict[str, Any]]
Yes
Hooks Overview -- Summary of all hooks
ShieldedPublicClient | null
Shielded public client for reads
walletClient
ShieldedWalletClient | null
Shielded wallet client for writes
address
Hex | null
Connected wallet address
error
string | null
Error message if initialization failed
loaded
boolean
Whether the wallet client has finished initializing
This hook must be used within a ShieldedWalletProvider. It will throw if called outside the provider tree.
TypeScript Ethereum library (used internally by wagmi)
seismic-viem
Seismic transport layer providing ShieldedPublicClient and ShieldedWalletClient
@tanstack/react-query
Async state management (required by wagmi)
ShieldedWalletProvider -- Provider reference and options
useShieldedWallet -- Access shielded wallet context
RainbowKit Guide -- Polished wallet modal alternative
Privy Guide -- Email/social login alternative
You need a WalletConnect Project ID from WalletConnect Cloud.
The envDir points to the monorepo root so that .env files at the top level are available to the web package.
Create a .env file at the monorepo root:
VITE_CHAIN_ID determines which chain the app connects to — 31337 is the local sanvil node.
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:
The provider stack nests four layers, each adding functionality:
WagmiProvider — manages wallet connections and chain state via wagmi hooks (useAccount, useConnect, etc.)
QueryClientProvider — provides React Query for caching and background data fetching
RainbowKitProvider — adds a polished wallet connect modal UI
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.
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:
Create src/main.tsx to bootstrap the app with theme and state management:
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.
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.
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.
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).
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.
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.
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.
The problem: Shielding hides a slot's value from RPC reads, but the value still participates in the storage Merkle trie that backs the account's storageHash. The trie key (the storage slot) is public, and the value is committed into the trie through a plain keccak hash with no per-slot salt. If a shielded value has low entropy — a boolean, a small enum, a value drawn from a known set — an attacker who knows the slot can guess candidate values, recompute the commitment, and confirm a match. The value is recovered without ever being read directly.
Why it leaks: A storage value enters the trie as roughly keccak256(RLP([slot_path, value])). With no salt, that commitment is a deterministic function of (slot, value), and the slot is known — so the only unknown is the value. The account's storageHash always exposes this commitment, but on its own it commits to the whole trie at once, so it is only practical to attack on freshly deployed or sparsely shielded contracts where the root covers just a handful of leaves.
Endpoints that return Merkle proofs (such as eth_getProof) make this much worse, and non-inclusion proofs are the sharpest case. A proof that a slot does not exist still returns the intermediate trie nodes along that slot's path. Because the trie key is keccak256(slot), an attacker can search for a junk, non-existent slot whose hash sits right next to your target's, request a non-inclusion proof for it, and pull back the branch node that directly commits to your private leaf. This isolates a single target leaf and makes it brute-forceable regardless of how full the trie is and without needing a conveniently located public slot nearby — defeating the intuition that a contract holding lots of shielded state is automatically safer. This is the same root cause as the Enums footgun, generalized to any low-entropy value.
What to do instead: Do not rely on shielding alone to protect a low-entropy secret stored at a known slot. If a value is sensitive and has a small domain, mix in high entropy before storing it — for example store keccak256(abi.encode(secret, salt)), where salt is itself a high-entropy shielded value, and compare against that commitment rather than the raw secret. This only works for values you check by equality, not ones you do arithmetic on. High-entropy shielded values — balances that accumulate over many transactions, for instance — are not practically guessable and need no special handling.
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.
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.
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.
npm install seismic-viem seismic-react viem wagmi @tanstack/react-queryimport { 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>
);
}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);
}
}// 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();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
},
);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>
)
}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>
)
}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}")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>
)
}npm install @rainbow-me/rainbowkitnpm install @reown/appkit @reown/appkit-adapter-wagmi wagmi viem @tanstack/react-query seismic-react seismic-viemimport { 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>
)
}declare namespace JSX {
interface IntrinsicElements {
"appkit-button": React.DetailedHTMLProps<
React.HTMLAttributes<HTMLElement>,
HTMLElement
>;
}
}cd packages
bun create vite web --template react-ts
cd webbun add seismic-react@1.1.1 seismic-viem@1.1.1 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@^4bun add -d @vitejs/plugin-react-swcimport { 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 Appimport { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
reducer: {},
})import { createTheme } from '@mui/material/styles'
const theme = createTheme({
palette: {
mode: 'dark',
},
})
export default themeimport ClownPuncher from '@/components/game/ClownPuncher'
const Home = () => <ClownPuncher />
export default Homeconst 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 shieldedExpenum Status { Active, Inactive, Suspended }
// Only 3 possible values (0, 1, 2) — shielding provides little protection
suint256 shieldedStatus = suint256(uint256(Status.Active));// The storage layout is public, so the slot is known, and an `sbool`
// has only two possible values — trivially guessable from the trie.
sbool private isWhitelisted;// 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");
}uint256The 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.
The encryption flow uses three of Seismic's precompiles:
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:
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.
The AES-GCM Encrypt precompile at address 0x66 encrypts the data:
Putting it all together in an internal helper:
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.
The recipient can decrypt the event data by performing the reverse of the encryption flow:
Take the contract's public key (stored on-chain and readable by anyone).
Combine it with their own private key using ECDH to derive the same shared secret.
Run HKDF with the same context string ("src20-transfer-event") to derive the same encryption key.
Decrypt the encryptedAmount from the event log using AES-GCM Decrypt.
Decrypt events client-side using the ECDH shared secret and AES-GCM decryption. See the seismic-viem precompiles documentation for the cryptographic primitives.
Here is the visibility breakdown for each piece of data in a Transfer event:
from address
Everyone
Indexed parameter, stored in public log topics
to address
Everyone
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.
// This will NOT compile
event Transfer(address indexed from, address indexed to, suint256 amount);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;
}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.
We will use OpenZeppelin's AccessControl to manage roles. This is a battle-tested pattern used across thousands of Ethereum contracts.
The contract implements three levels of access:
Regular user
View their own balance
getBalance(myAddress) via signed read
Auditor (AUDITOR_ROLE)
View any account's balance (read-only)
The deployer holds DEFAULT_ADMIN_ROLE and can grant roles to other addresses:
In TypeScript:
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.
Even with compliance roles in place, the privacy model is strong:
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.
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.
No broadcast disclosure. 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.
// 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);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
seismicTestnet, sanvil, createSeismicDevnet chain configs
Encryption
ECDH key exchange, AES-GCM calldata encryption
Precompiles
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 (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
Create a wallet client -> Shielded Wallet Client
Create a read-only client -> Shielded Public Client
Interact with a contract -> Contract Instance
Send an encrypted transaction -> Shielded Writes
Read with identity proof -> Signed Reads
Connect to a network ->
Understand calldata encryption -> Encryption
Install the package ->
Client types -> Shielded Public Client, Shielded Wallet Client
Contract interaction -> Contract Instance, Shielded Writes, Signed Reads
Chains -> Chains (seismicTestnet, sanvil, createSeismicDevnet)
Encryption -> Encryption (getEncryption, AesGcmCrypto)
Precompiles -> Precompiles (rng, ecdh, aesGcmEncrypt, hkdf, secp256k1Sig)
Level
Low-level SDK
React hooks layer
Framework
Framework-agnostic TypeScript
Install seismic-viem -- Add the package to your project
Configure a chain -- Connect to testnet or local dev
Create a shielded wallet client -- Connect with full capabilities
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
npm install seismic-viem viemimport { 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..."],
});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 generationInstall seismic-viem, configure viem peer dependency
Chain configuration (SEISMIC_TESTNET, SANVIL)
Interact with shielded and public contracts
Step-by-step tutorials and runnable examples
Complete API documentation for all types and functions
Primitive types (Bytes32, PrivateKey, etc.)
w3.seismic namespace methods
Privacy-preserving cryptographic functions
Send a shielded transaction → Shielded Write Guide
Execute a signed read → Signed Reads Guide
Work with SRC20 tokens → SRC20 Documentation
Use async client → Client Documentation
Configure custom chain →
Client factories → create_wallet_client, create_public_client
Contract wrappers → ShieldedContract, PublicContract
Encryption → EncryptionState, Precompiles
Transaction types → ,
🔒 Shielded Transactions - Encrypt calldata with TEE public key
📝 Signed Reads - Prove identity in eth_call
🪙 SRC20 Support - Built-in support for private tokens
⚡ 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
The SDK extends web3.py with a custom w3.seismic namespace:
Install and setup a client - Get connected to Seismic
Send your first shielded transaction - Step-by-step guide
Explore the API reference - Deep dive into all types and functions
pip install seismic-web3uv add seismic-web3import 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()Create sync/async wallet and public clients
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)Abi
Yes
Contract ABI
address
Address
Yes
Contract address
contract
ShieldedContract | null
The shielded contract instance
abi
Abi
Once you have a ShieldedContract instance, call its methods directly for both signed reads and shielded writes:
For full type inference on contract methods, define your ABI with as const:
useShieldedWallet -- Access the underlying wallet and public clients
useShieldedWriteContract -- Send encrypted writes without a contract instance
useSignedReadContract -- Perform signed reads without a contract instance
ShieldedWalletProvider -- Context provider required by this hook
-- Summary of all hooks
import { useShieldedContract } from "seismic-react";abi
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..." });Signed read from a contract (authenticated query)
Node.js 18+
A WalletConnect project ID
Install dependencies:
Create the wagmi configuration with the Seismic testnet chain:
Wrap your app with the required providers. ShieldedWalletProvider must be nested inside the wagmi and React Query providers:
Create a component that uses useShieldedWriteContract and useSignedReadContract to interact with a shielded counter contract:
Combine the wallet connection button with the counter component. The counter only renders once the shielded wallet is ready:
RainbowKit handles wallet connection and chain switching
ShieldedWalletProvider automatically creates shielded clients when a wallet connects, performing the ECDH key exchange with the TEE
useShieldedWriteContract encrypts calldata before sending the transaction, ensuring on-chain privacy
useSignedReadContract attaches a signature proving caller identity, allowing the contract to return private data only to authorized readers
Hooks Reference - Full API for all hooks
Wallet Guides - Use AppKit or Privy instead of RainbowKit
Chains - Configure Seismic testnet or local Sanvil
ShieldedWalletProvider - Provider configuration options
useShieldedWriteContract - Shielded write hook API
useSignedReadContract - Signed read hook API
Installation - Full dependency setup
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 resultReplace YOUR_WALLETCONNECT_PROJECT_ID with your actual project ID from .
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.
Every shielded transaction follows this pipeline:
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 fieldsgetEncryption(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.
import { getEncryption } from "seismic-viem";The ShieldedWalletClient exposes encryption operations as client methods. These use the AES key that was derived during client construction.
Every Seismic transaction includes additional fields that carry encryption metadata alongside the standard Ethereum transaction fields.
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.
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
For EIP-712 signing, the TxSeismic typed-data message also includes authorizationListHash = keccak256(rlp(authorizationList)). This binds the EIP-7702 authorization list carried on the transaction wire format into the signature. The hash for an empty authorization list is 0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347.
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.
-- Client that performs encryption automatically
-- How encrypted transactions are built and sent
-- Authenticated reads that prove caller identity
-- On-chain cryptographic operations via precompiled contracts
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.
import { getShieldedContract } from "seismic-viem";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.
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.
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.
See for details on how signed reads work under the hood.
Always encrypts calldata before broadcasting the transaction, regardless of whether the function has shielded parameters. The calldata is not visible on-chain.
See for the encryption lifecycle and security parameters.
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.
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).
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.
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.
-- Encryption lifecycle and shieldedWriteContract
-- Authenticated reads and signedReadContract
-- Creating the client passed to getShieldedContract
React context provider for shielded wallet and public client
React context component that wraps wagmi's connector client to provide ShieldedPublicClient and ShieldedWalletClient from seismic-viem. This is the core provider that all Seismic React hooks depend on.
import { ShieldedWalletProvider } from "seismic-react";The provider exposes a WalletClientContextType to all descendant components via React context:
For Next.js, create the provider in a client component:
Then wrap your layout:
The providers must be nested in this order:
The provider follows this initialization sequence:
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.
Wallet client creation -- When a wallet connects through wagmi, the provider creates a ShieldedWalletClient from the connector's client. This enables encrypted write transactions.
The onAddressChange callback is useful for initializing state when a wallet connects:
The provider captures errors during client creation and exposes them through the context:
Access the error state through useShieldedWallet:
-- Package setup and peer dependencies
-- Hook to consume the provider context
-- All available hooks
-- Wallet UI integration
Create sync Web3 instance with full Seismic wallet capabilities
Create a synchronous Web3 instance with full Seismic wallet capabilities.
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 ECDH, 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.
The function performs five steps:
Create Web3 instance
Fetch TEE public key (synchronous RPC call)
Generate encryption keypair (if encryption_sk is None, a random ephemeral key is created)
The returned client provides:
get_block(), get_transaction(), get_balance()
send_raw_transaction(), wait_for_transaction_receipt()
- Send shielded transactions
- Debug shielded transactions
- Execute signed reads
- Deposit ETH/tokens
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
Uses this key to encrypt all shielded transaction calldata and signed reads
Access the encryption state at w3.seismic.encryption if needed for advanced use cases.
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
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
HTTPS recommended - Use HTTPS URLs in production to prevent MITM attacks
- Async variant with WebSocket support
- Read-only client without private key
- Encryption state class
- Encryption derivation function
Derive encryption state from TEE public key using ECDH
Derive encryption state from a TEE public key using ECDH key exchange.
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 EncryptionState 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.
def get_encryption(
network_pk: CompressedPublicKey,
client_sk: PrivateKey | None = None,
) -> EncryptionStateThe function performs three steps:
Generate client key if needed
Derive AES key via ECDH +
This performs:
ECDH: Compute shared secret from client_sk and network_pk
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.
The SDK defaults to random ephemeral keys for simplicity. Use deterministic keys only if you need to recreate the same encryption state across sessions.
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)
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)
- Returned encryption state class
- Sync client factory (calls get_encryption)
- Async client factory
- Client private key type
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.
pip install seismic-web3Or with uv:
uv add seismic-web3import os
from seismic_web3 import SEISMIC_TESTNET, PrivateKey
pk = PrivateKey.from_hex_str(os.environ["PRIVATE_KEY"])
w3 = SEISMIC_TESTNET.wallet_client(pk)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.
Every method on w3.seismic and on ShieldedContract is await-able when using the async 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).
The recommended approach is to use chain configuration objects:
See for more details.
The wallet client automatically handles encryption setup:
Fetches the network's TEE public key
Derives a shared key via
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.
- Send shielded transactions
- Debug shielded transactions
- Execute signed reads
- Get TEE public key
- Get TEE public key
- Query deposit merkle root
- Query deposit count
- Create contract wrappers (.tread only)
- Working with shielded and public contracts
- Detailed w3.seismic namespace documentation
- Chain configs and constants
- Step-by-step shielded transaction guide
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.
When you instantiate a contract:
You get access to five namespaces:
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.
Indexed parameter, stored in public log topics
Transfer amount
Recipient only
Encrypted to recipient's public key
A transfer happened
Everyone
The event emission itself is visible
auditBalanceOf(account) via signed read
Compliance officer (COMPLIANCE_ROLE)
View any balance, freeze/unfreeze accounts
complianceBalanceOf(account) via signed read; complianceFreeze(account) via transaction
The ABI passed in
address
Address
The address passed in
error
Error | null
Error if wallet client not initialized
Seismic transaction dataclasses
EIP-712 typed data signing functions
SRC20 token standard support
Built-in contract ABIs and helpers
Prove caller identity in eth_call via seismic_call
RNG, ECDH, AES-GCM, HKDF, secp256k1 signing precompile bindings
React 18+
Foundation
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
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
value -- ETH valueseismicElements -- the encryptionPubkey, encryptionNonce, messageVersion, recentBlockHash, expiresAtBlock, and signedRead fields
Installation -- Bundled crypto dependencies
networkPk
string
Yes
TEE's secp256k1 public key (fetched via client.getTeePublicKey())
clientSk
Hex
No
Client's encryption private key. If omitted, generates a random keypair
aesKey
Hex
Derived AES-256 key for encrypting calldata
encryptionPrivateKey
Hex
getEncryption()
Hex
Returns the AES-256 key used for shielded operations
getEncryptionPublicKey()
Hex
encryptionPubkey
Hex
Client's compressed secp256k1 public key
encryptionNonce
Hex
@noble/curves
secp256k1 keypair generation and ECDH
@noble/ciphers
AES-GCM encryption and decryption
@noble/hashes
Client's ephemeral secp256k1 private key
Returns the client's compressed secp256k1 encryption public key
Random 12-byte AES-GCM nonce
SHA-256, HKDF, and other hash functions
.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 -- ECDH key exchange and AES-GCM calldata encryption
abi
Abi
Yes
Contract ABI (use as const for full type inference)
address
Address
Yes
Deployed contract address
client
ShieldedWalletClient | keyed client
Yes
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.
.read
Smart Read
Auto-detected
Auto-detected
.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.
Inspects ABI -- shielded if shielded params, else transparent
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 callback -- If provided, called after both clients are ready with the new address. Use this to load user-specific data or initialize contract state.
Reconnection handling -- When the user switches accounts or reconnects, the provider recreates the wallet client and fires onAddressChange again.
onAddressChange throws
Error in your callback
The error is caught and set on context.error
Privy Guide -- Embedded wallet setup
children
React.ReactNode
Yes
Child components that can access shielded wallet context
config
Config (from wagmi)
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
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
Public client creation fails
Invalid chain config or transport
Check wagmi config and chain definitions
Wallet client creation fails
Connector incompatibility or network mismatch
ShieldedWalletProvider must be inside WagmiProvider because it reads the connected wallet from wagmi's context. Placing it outside will cause a runtime error.
Connected wallet address. null when no wallet is connected.
Ensure the wallet supports the target chain
HKDF: Derive 32-byte AES key from shared secret
Derive client public key
Return EncryptionState
The AES key is derived using ECDH + HKDF (NIST SP 800-56C)
Called automatically by create_wallet_client() and create_async_wallet_client()
You rarely need to call this directly unless implementing custom client logic
ECDH security - Based on secp256k1 elliptic curve discrete logarithm problem
HKDF - Uses SHA-256 for key derivation
CompressedPublicKey - TEE public key type
Shielded Write Guide - How encryption is used in transactions
network_pk
Yes
The TEE's 33-byte compressed secp256k1 public key
client_sk
No
Optional 32-byte client private key. If None, a random ephemeral key is generated
Fully initialized encryption state with AES key and keypair
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
deposit() - Deposit ETH/tokens
get_deposit_root() - Query deposit merkle root
get_deposit_count() - Query deposit count
contract() - Create contract wrappers
Sync
Create sync wallet client from RPC URL
Async
Sync
Create sync public client from RPC URL
Async
Holds AES key and encryption keypair
Derives encryption state from TEE public key
Create async wallet client from RPC URL
Create async public client from RPC URL
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);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..."]);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 Appimport { 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>
}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,
)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_keyimport 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_skif client_sk is None:
client_sk = PrivateKey(os.urandom(32))aes_key = generate_aes_key(client_sk, network_pk)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 keyfrom 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)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)Yes
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
Attach Seismic namespace
w3.seismic = SeismicNamespace(w3, encryption, private_key)web3.pyget_tee_public_key() - Get TEE public key
get_deposit_root() - Query deposit merkle root
get_deposit_count() - Query deposit count
contract() - Create contract wrappers
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()
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,
) -> Web3rpc_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
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_idw3 = Web3(Web3.HTTPProvider(rpc_url))network_pk = get_tee_public_key(w3)encryption_sk = encryption_sk or PrivateKey(os.urandom(32))private_key
encryption = get_encryption(network_pk, encryption_sk)Broadcasts
Use Case
Write
Yes
Your address
Yes
Privacy-sensitive writes
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
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)
Higher gas cost (encryption overhead)
Transparent (.twrite, .tread):
Calldata is visible on-chain
Standard Ethereum transactions/calls
No encryption or security metadata
Lower gas cost (no overhead)
Faster (no encryption computation)
Write (.write, .twrite, .dwrite):
Broadcasts a transaction
Consumes gas
Modifies contract state
Returns transaction hash
Must wait for confirmation
Read (.read, .tread):
Executes an eth_call
Free (no gas cost)
Does not modify state
Returns result immediately
No confirmation needed
When to use:
Amounts, addresses, or arguments should be private
Privacy compliance required
Trading, voting, auctions, confidential transfers
When to use:
Contract checks msg.sender for access control
Caller-specific data (e.g., "my balance")
Privacy required for queries
Result should be encrypted
When to use:
Data is public anyway
Lower gas cost matters
No privacy requirements
Standard Ethereum behavior
When to use:
Function is public (doesn't check msg.sender)
No authentication needed
Data is public
When to use:
Development and testing
Debugging encryption issues
Verifying calldata encoding
Auditing transaction parameters
Problem:
Solution:
Unnecessary overhead:
Better:
Contract Instance — Contract wrapper reference
Shielded Write Guide — Full encryption workflow
Signed Read Guide — Authentication and privacy for reads
SeismicSecurityParams — Security parameters
— Debug write return type
contract = w3.seismic.contract(address="0x...", abi=ABI)Namespace
Operation
Encryption
msg.sender
# 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)Seismic Testnet
seismicTestnet
5124
https://testnet-1.seismictest.net/rpc
Public testnet
Sanvil
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
The public testnet for development and testing against a live Seismic network:
Local development chain using Sanvil (Seismic's fork of Anvil). Chain ID 31337 matches Anvil/Hardhat defaults:
For running a local seismic-reth --dev node. Uses chain ID 5124 (same as testnet) but connects to localhost:
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.
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.
Create a custom chain definition for any Seismic-compatible node:
nodeHost
string
Yes
Base URL of the Seismic node (without /rpc)
The factory returns a viem Chain object with:
Chain ID 5124
RPC URL set to {nodeHost}/rpc
Seismic chain formatters included
Optional block explorer configuration
Convenience factories for common Seismic infrastructure:
These generate chain configs pointing to numbered Seismic testnet instances on Azure and GCP infrastructure respectively.
Seismic chain configs use a custom transaction request type that extends viem's standard TransactionRequest with Seismic-specific fields:
encryptionPubkey
Hex
Client's compressed secp256k1 public key
encryptionNonce
Hex
Installation -- Install seismic-viem and viem
Seismic Viem Overview -- Full SDK overview and architecture
Shielded Wallet Client -- Create a client using a chain config
Encryption -- How calldata encryption uses chain-level formatters
import {
seismicTestnet,
sanvil,
localSeismicDevnet,
createSeismicDevnet,
} from "seismic-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..."),
});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
}Sync contract wrapper with shielded and transparent namespaces
Synchronous contract wrapper providing encrypted and transparent interaction with Seismic contracts.
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.
class ShieldedContract:
def __init__(
self,
w3: Web3,
encryption: EncryptionState,
private_key: PrivateKey,
address: ChecksumAddress,
abi: list[dict[str, Any]],
eip712: bool = False,
) -> None:
...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)
gas_price: int | None
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)
security: [
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.)
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
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
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
- Async version of this class
- Read-only contract wrapper (no encryption)
- Manages encryption keys
- Debug write return type
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.
Create async Web3 instance with full Seismic wallet capabilities
Create an asynchronous AsyncWeb3 instance with full Seismic wallet capabilities.
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 , and attaches a fully-configured 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.
Holds AES key and encryption keypair derived from ECDH
Holds the -GCM key and encryption keypair derived from key exchange.
EncryptionState encapsulates all cryptographic material needed for shielded transactions and signed reads. It's created by during wallet client setup and attached to .encryption.
The class provides and methods that handle AES-GCM encryption with metadata-bound Additional Authenticated Data (AAD).
Create sync Web3 instance with public (read-only) Seismic access
Create a synchronous Web3 instance with public (read-only) Seismic access.
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 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.
explorerUrl
string
No
Block explorer URL for the chain
Random nonce for AES-GCM encryption of calldata
messageVersion
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
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)
security: [SeismicSecurityParams](../api-reference/transaction-types/seismic-security-params.md) | None - Security parameters for expiry
](../api-reference/transaction-types/seismic-security-params.md) | NoneGas 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
SeismicSecurityParams - Transaction expiry parameters
Contract Namespaces - Detailed namespace documentation
Shielded Write Guide - Complete workflow guide
w3
Web3
Yes
Synchronous Web3 instance connected to Seismic RPC
encryption
Yes
Encryption state for shielded operations
private_key
Yes
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)Yes
Chain configuration (e.g., seismicTestnet)
transport
Transport
Yes
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
Promise<ShieldedWalletClient>
A viem Client extended with PublicActions, WalletActions, EncryptionActions, ShieldedPublicActions, ShieldedWalletActions, DepositContractPublicActions, DepositContractWalletActions, and SRC20WalletActions.
When you call createShieldedWalletClient(), the following steps happen:
Creates a ShieldedPublicClient (or reuses the one provided via publicClient)
Fetches the TEE public key from the node via seismic_getTeePublicKey RPC
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
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)
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)
The wallet client also includes all actions from ShieldedPublicClient:
Shielded public actions -- getTeePublicKey(), explorerUrl(), etc.
Precompile actions -- rng(), ecdh(), aesGcmEncryption(), aesGcmDecryption(), hdfk(), 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.
The encryption derivation logic is also available as a standalone function, separate from any client:
networkPk
Hex
Yes
The TEE's secp256k1 public key
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.
Shielded Public Client -- Read-only client without private key
Deposit Contract -- Validator staking via deposit()
SRC20 Event Watching -- Watch and decrypt SRC20 token events
Encryption -- Encryption utilities and getEncryption()
-- Chain configurations for Seismic networks
-- Precompile details and parameters
-- Working with contract wrappers
import { createShieldedWalletClient } from "seismic-viem";const walletClient = await createShieldedWalletClient({
chain: seismicTestnet,
transport: http(),
account: privateKeyToAccount("0x..."),
});chain
Chain
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 keyprovider_url
str
Yes
HTTP(S) or WS(S) URL of the Seismic node
private_key
AsyncWeb3
An AsyncWeb3 instance with w3.seismic namespace attached ()
The function performs six steps:
Create provider
if ws:
provider = WebSocketProvider(provider_url)
else:
provider = AsyncHTTPProvider(provider_url)Create AsyncWeb3 instance
w3 = AsyncWeb3(provider)Fetch TEE public key (async RPC call)
network_pk = await async_get_tee_public_key(w3)Generate encryption keypair (if encryption_sk is None, a random ephemeral key is created)
Derive encryption state (ECDH + )
Attach Seismic namespace
The returned client provides:
await get_block(), await get_transaction(), await get_balance()
await send_raw_transaction(), await wait_for_transaction_receipt()
All other standard async web3.py functionality
await send_shielded_transaction() - Send shielded transactions
await debug_send_shielded_transaction() - Debug shielded transactions
await signed_call() - Execute signed reads
await deposit() - Deposit ETH/tokens
- Get TEE public key
- Query deposit merkle root
- Query deposit count
- Create contract wrappers (methods are async)
Aspect
AsyncHTTPProvider (ws=False)
WebSocketProvider (ws=True)
Connection
New connection per request
Persistent connection
Latency
Higher per-request overhead
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
Uses this key to encrypt all shielded transaction calldata and signed reads
Access the encryption state at w3.seismic.encryption if needed for advanced use cases.
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, 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
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
HTTPS/WSS recommended - Use secure protocols in production to prevent MITM attacks
create_wallet_client - Sync variant (HTTP only)
create_async_public_client - Async read-only client
EncryptionState - Encryption state class
get_encryption - Encryption derivation function
- The async w3.seismic namespace
- Pre-configured chain constants
async def create_async_wallet_client(
provider_url: str,
private_key: PrivateKey,
*,
encryption_sk: PrivateKey | None = None,
ws: bool = False,
) -> AsyncWeb3import 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 w3aes_key
32-byte AES-256 key derived from ECDH +
encryption_pubkey
Client's 33-byte compressed secp256k1 public key
encryption_private_key
Client's 32-byte secp256k1 private key
Encrypt plaintext calldata with metadata-bound AAD.
plaintext
HexBytes
Raw calldata to encrypt
nonce
HexBytes
Ciphertext with 16-byte authentication tag appended
Decrypt ciphertext with metadata-bound AAD.
ciphertext
HexBytes
Encrypted data (includes 16-byte auth tag)
nonce
HexBytes
Decrypted plaintext
cryptography.exceptions.InvalidTag - If authentication fails (wrong key, tampered data, or mismatched metadata)
When created, EncryptionState automatically initializes an internal AesGcmCrypto instance:
Encode metadata as AAD using encode_metadata_as_aad()
Call AesGcmCrypto.encrypt(plaintext, nonce, aad)
Return ciphertext with 16-byte authentication tag
Encode metadata as AAD
Call AesGcmCrypto.decrypt(ciphertext, nonce, aad)
Verify authentication tag (raises InvalidTag if fails)
Return plaintext
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.
Pure computation - no I/O operations
Works in both sync and async contexts
Created automatically by create_wallet_client() and create_async_wallet_client()
The internal _crypto field is excluded from repr() and comparison
Authentication tag is always 16 bytes (AES-GCM standard)
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
get_encryption - Derive encryption state from TEE public key
create_wallet_client - Sync client factory (creates EncryptionState)
create_async_wallet_client - Async client factory
EncryptionNonce - 12-byte nonce type
- Metadata structure
- How shielded transactions work
@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: PrivateKeydef encrypt(
self,
plaintext: HexBytes,
nonce: EncryptionNonce,
metadata: TxSeismicMetadata,
) -> HexBytesfrom 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) + 16def decrypt(
self,
ciphertext: HexBytes,
nonce: EncryptionNonce,
metadata: TxSeismicMetadata,
) -> HexBytesfrom 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 == plaintextimport 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 laterfrom 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)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
Web3
A Web3 instance with w3.seismic namespace attached ()
The function performs two steps:
Create Web3 instance
w3 = Web3(Web3.HTTPProvider(rpc_url))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.
get_block(), get_transaction(), get_balance()
get_code(), call(), estimate_gas()
All other standard read-only web3.py functionality
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)
send_shielded_transaction() - Requires private key
debug_send_shielded_transaction() - Requires private key
signed_call() - Requires private key and encryption
deposit() - Requires private key
Contract .swrite and .sread methods - Require private key
Private key
Not required
Required
Shielded writes
No
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
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
create_async_public_client - Async variant with WebSocket support
create_wallet_client - Full-featured client with private key
SeismicPublicNamespace - The public w3.seismic namespace
Chains Configuration - Pre-configured chain constants
- Working with contract wrappers
def create_public_client(rpc_url: str) -> Web3from 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 clientfrom 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)Async contract wrapper with shielded and transparent namespaces
Asynchronous contract wrapper providing encrypted and transparent interaction with Seismic contracts.
AsyncShieldedContract is the async version of ShieldedContract, providing the same five namespaces (.write, .read, .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.
class AsyncShieldedContract:
def __init__(
self,
w3: AsyncWeb3,
encryption: EncryptionState,
private_key: PrivateKey,
address: ChecksumAddress,
abi: list[dict[str, Any]],
eip712: bool = False,
) -> None:
...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
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: [
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.)
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
Like .write but returns debug information including plaintext and encrypted views. Transaction is actually broadcast.
Returns: Coroutine[DebugWriteResult] ()
Positional Arguments: *args - ABI function arguments
Optional Parameters: Same as .write
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
- Synchronous version of this class
- Async read-only contract wrapper
- Create async client
- Manages encryption keys
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.
Create async Web3 instance with public (read-only) Seismic access
Create an asynchronous AsyncWeb3 instance with public (read-only) Seismic access.
create_async_public_client() creates an async client for read-only operations on the Seismic network. No private key is required. The namespace provides only public read operations: , , , and (with .tread only).
Supports both HTTP and WebSocket connections for efficient async queries and real-time monitoring.
Transparent write -- always sends with plaintext calldata (unencrypted)
dwriteContract(params)
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 encryption using the derived key
decrypt(ciphertext, metadata)
Manual AES-GCM decryption using the derived key
clientSk
Hex
No
Client's encryption private key. If not provided, generates a random one
12-byte AES-GCM nonce
metadata
Transaction metadata (used to build AAD)
12-byte AES-GCM nonce
metadata
Transaction metadata (used to build AAD)
Yes
Signed reads
No
Yes
Transparent reads
Yes
Yes
Deposits
No
Yes
TEE queries
Yes
Yes
Standard Web3
All read operations
All operations
Yes
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
Lower latency
Subscriptions
Not supported
Supported (eth.subscribe)
Resource usage
Lower idle usage
Keeps connection open
Use case
One-off transactions
Real-time monitoring, subscriptions
encryption_sk = encryption_sk or PrivateKey(os.urandom(32))encryption = get_encryption(network_pk, encryption_sk)w3.seismic = AsyncSeismicNamespace(w3, encryption, private_key)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)
security: [SeismicSecurityParams](../api-reference/transaction-types/seismic-security-params.md) | None - Security parameters for expiry
](../api-reference/transaction-types/seismic-security-params.md) | NoneABI 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
DebugWriteResult - Debug write return type
SeismicSecurityParams - Transaction expiry parameters
Contract Namespaces - Detailed namespace documentation
w3
AsyncWeb3
Yes
Asynchronous AsyncWeb3 instance connected to Seismic RPC
encryption
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 closedYes
Chain
Yes
Chain configuration (e.g., seismicTestnet)
transport
Transport
Yes
viem transport (e.g., http())
ShieldedPublicClient
A viem PublicClient extended with ShieldedPublicActions, DepositContractPublicActions, and SRC20PublicActions.
getTeePublicKey()
Promise<Hex>
Fetch the TEE's secp256k1 public key via seismic_getTeePublicKey RPC
getStorageAt()
--
rng(params)
Promise<bigint>
Generate random numbers via the RNG precompile
ecdh(params)
Promise<Hex>
watchSRC20EventsWithKey()
Promise<() => void>
Watch for SRC20 events with a decryption key
See SRC20 Event Watching for the full flow, log types, and example usage.
getDepositRoot()
Promise<Hex>
Query the deposit merkle root
getDepositCount()
Promise<Hex>
See Deposit Contract for parameters and validator registration via deposit().
All standard viem PublicActions are available directly on the client:
getBlockNumber(), getBlock(), getTransaction()
getBalance(), getCode(), call()
estimateGas(), waitForTransactionReceipt()
All other viem public client methods
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.
txExplorerUrl
TxExplorerTab
'index', 'token_transfers', 'internal', 'logs', 'state', 'raw_trace'
addressExplorerUrl
AddressExplorerTab
The same helpers are exported as standalone functions that take an explicit chain and can be used without a client:
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
Chains -- 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(),
});chain
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",
});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.
provider_url
str
Yes
HTTP(S) or WS(S) URL of the Seismic node
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
AsyncWeb3
An AsyncWeb3 instance with w3.seismic namespace attached ()
The function performs three steps:
Create provider
if ws:
provider = WebSocketProvider(provider_url)
else:
provider = AsyncHTTPProvider(provider_url)Create AsyncWeb3 instance
w3 = AsyncWeb3(provider)Attach public Seismic namespace
w3.seismic = AsyncSeismicPublicNamespace(w3)No TEE public key fetching or encryption setup is performed since the client cannot perform shielded operations.
await get_block(), await get_transaction(), await get_balance()
await call(), await estimate_gas()
All other standard read-only async web3.py functionality
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 (.tread methods are async)
send_shielded_transaction() - Requires private key
debug_send_shielded_transaction() - Requires private key
signed_call() - Requires private key and encryption
deposit() - Requires private key
Contract .swrite and .sread methods - Require private key
Aspect
AsyncHTTPProvider (ws=False)
WebSocketProvider (ws=True)
Connection
New connection per request
Persistent connection
Latency
Higher per-request overhead
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
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
For sync operations, use
Async block explorers and chain analytics
Real-time monitoring dashboards with WebSocket subscriptions
High-throughput read-only services
Async data aggregation pipelines
Event monitoring and alerting systems
Price oracles with low-latency requirements
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
create_public_client - Sync variant (HTTP only)
create_async_wallet_client - Async client with private key
AsyncSeismicPublicNamespace - The async public w3.seismic namespace
Chains Configuration - Pre-configured chain constants
- Working with contract wrappers
def create_async_public_client(
provider_url: str,
*,
ws: bool = False,
) -> AsyncWeb3from 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']}")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
First, copy the compiled ABI from your contracts build output into the web package. After deploying (see ), copy the ABI file:
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.
Create src/types/contract.ts
Throws error (not supported on Seismic)
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
ECDH key exchange via precompile
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
Query the deposit count
'txs', 'token_transfers', 'tokens', 'internal_txns', 'coin_balance_history', 'logs', 'contract'
tokenExplorerUrl
TokenExplorerTab
'token_transfers', 'holders', 'contract'
blockExplorerUrl
BlockExplorerTab
'index', 'txs'
Lower latency
Subscriptions
Not supported
Supported (eth.subscribe)
Resource usage
Lower idle usage
Keeps connection open
Use case
One-off queries
Real-time monitoring, subscriptions
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.
This hook wraps the contract methods into callable functions with proper error handling. Create src/hooks/useContractClient.ts:
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 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.
This distinction between twrite, read, and tread is the key difference from a standard Ethereum dApp.
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:
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
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
mkdir -p packages/web/src/abis/contracts
cp packages/contracts/out/ClownBeatdown.sol/ClownBeatdown.json \
packages/web/src/abis/contracts/ClownBeatdown.json{
"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,
};
};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.
import {
rng,
ecdh,
aesGcmEncrypt,
aesGcmDecrypt,
hdfk,
secp256k1Sig,
} from "seismic-viem";Generates cryptographically secure random numbers using the TEE's CSPRNG.
bigint -- the generated random value.
Performs an ECDH key exchange inside the TEE and returns the shared secret.
Hex -- 32-byte shared secret.
Performs AES-GCM encryption and decryption inside the TEE.
Hex -- the AES-GCM ciphertext.
string -- the decrypted plaintext.
Derives a key from input key material using HKDF inside the TEE.
Hex -- the derived key.
Generates a secp256k1 signature inside the TEE.
Signature -- a viem Signature object with r, s, and v components.
All precompiles are available as methods directly on ShieldedPublicClient and ShieldedWalletClient, so you can call them without importing the standalone functions.
Each precompile is defined as a Precompile<P, R> object with a standard interface. You can invoke any precompile directly using its object.
The CallClient type accepts any client with a .call() method, so both ShieldedPublicClient and ShieldedWalletClient work with 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.
-- Read-only client with precompile methods
-- Full-featured client with precompile methods
-- Client-side ECDH key exchange and AES-GCM encryption
-- Chain configurations for Seismic networks
rng
0x...0064
Random number generation
numBytes, pers?
bigint
ecdh
0x...0065
ECDH key exchange
sk, pk
Hex (shared secret)
import { rng } from "seismic-viem";
const randomValue = await rng(client, {
numBytes: 32,
});
console.log("Random value:", randomValue);numBytes
bigint | number
Yes
Number of random bytes to generate (1--32)
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);sk
Hex
Yes
32-byte secp256k1 secret key
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);aesKey
Hex
Yes
32-byte AES-256 key
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);aesKey
Hex
Yes
32-byte AES-256 key
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);ikm
Hex
Yes
Input key material
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);sk
Hex
Yes
32-byte secp256k1 secret key
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);client.rng(params)
rng(client, params)
Random number generation
client.ecdh(params)
ecdh(client, params)
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...");address
Hex
Fixed address of the precompile contract
gasCost(args)
(args: P) => bigint
rngPrecompile
Precompile<RngParams, bigint>
ecdhPrecompile
Precompile<EcdhParams, Hex>
aesGcmEncryptPrecompile
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 },
});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
pers
Hex | ByteArray
No
Personalization string to seed the CSPRNG
pk
Hex
Yes
33-byte compressed secp256k1 public key
nonce
number
Yes
Numeric nonce for AES-GCM
plaintext
string
Yes
Plaintext string to encrypt
nonce
number
Yes
Nonce used during encryption
ciphertext
Hex
Yes
Ciphertext to decrypt
message
string
Yes
Message to sign
ECDH key exchange
client.aesGcmEncryption(params)
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
Computes the gas cost for the given arguments
encodeParams(args)
(args: P) => Hex
ABI-encodes the arguments for the call
decodeResult(result)
(result: Hex) => R
Decodes the raw call result
Precompile<AesGcmEncryptionParams, Hex>
aesGcmDecryptPrecompile
Precompile<AesGcmDecryptionParams, string>
hdfkPrecompile
Precompile<Hex, Hex>
secp256k1SigPrecompile
Precompile<Secp256K1SigParams, Signature>
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
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 ShowClownThe 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.
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)
Clown KO — show Reset button (right), Rob button now callable (left)
This component ties everything together. Create src/components/game/ClownPuncher.tsx:
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.
Create the auth context and wallet button used throughout the app. Create src/components/chain/WalletConnectButton.tsx:
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!
Connect wallet — RainbowKit modal, ShieldedWalletProvider derives shielded keys
Hit the clown — twrite.hit() sends a shielded transaction, stamina decrements
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.
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
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 ClownPuncherimport 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 EntryScreenimport 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 WalletConnectButtoncd packages/web
bun dev