From ritual-dapp-skills
Configures Ritual Chain (ID 1979) for dApp deployment with viem/wagmi setups, RPCs, block explorer, and EIP-1559 transaction requirements.
How this skill is triggered — by the user, by Claude, or both
Slash command
/ritual-dapp-skills:ritual-dapp-deployThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
| Parameter | Value |
| Parameter | Value |
|---|---|
| Chain ID | 1979 |
| Chain Name | Ritual |
| Native Currency | RITUAL (18 decimals, testnet, no real value) |
| RPC (HTTP) | https://rpc.ritualfoundation.org |
| RPC (WebSocket) | wss://rpc.ritualfoundation.org/ws |
| Block Explorer | https://explorer.ritualfoundation.org |
EIP-1559 only. Ritual Chain requires EIP-1559 (type-2) transactions. Legacy (type-0) transactions are rejected with
transaction type not supported. Do NOT use--legacyflag with forge/cast, and ensure your web3 library sends EIP-1559 transactions (viem does this by default; web3.py and ethers.js may need explicit configuration).
Create viem clients configured for Ritual Chain:
import { createPublicClient, createWalletClient, http, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const ritualChain = defineChain({
id: 1979,
name: 'Ritual',
nativeCurrency: { name: 'RITUAL', symbol: 'RITUAL', decimals: 18 },
rpcUrls: {
default: {
http: [process.env.RITUAL_RPC_URL || 'https://rpc.ritualfoundation.org'],
webSocket: [process.env.RITUAL_WS_URL || 'wss://rpc.ritualfoundation.org/ws'],
},
},
blockExplorers: {
default: { name: 'Ritual Explorer', url: 'https://explorer.ritualfoundation.org' },
},
contracts: {
multicall3: { address: '0x5577Ea679673Ec7508E9524100a188E7600202a3' },
},
});
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const publicClient = createPublicClient({ chain: ritualChain, transport: http() });
const walletClient = createWalletClient({ account, chain: ritualChain, transport: http() });
A standalone chain definition for use in wagmi config or other contexts:
import { defineChain } from 'viem';
export const ritualChain = defineChain({
id: 1979,
name: 'Ritual',
nativeCurrency: {
decimals: 18,
name: 'Ritual',
symbol: 'RITUAL',
},
rpcUrls: {
default: {
http: ['https://rpc.ritualfoundation.org'],
webSocket: ['wss://rpc.ritualfoundation.org/ws'],
},
},
blockExplorers: {
default: {
name: 'Ritual Explorer',
url: 'https://explorer.ritualfoundation.org',
},
},
contracts: {
multicall3: {
address: '0x5577Ea679673Ec7508E9524100a188E7600202a3',
},
},
});
import { http, createConfig } from 'wagmi';
import { injected, walletConnect } from 'wagmi/connectors';
import { defineChain } from 'viem';
const ritualChain = defineChain({
id: 1979,
name: 'Ritual',
nativeCurrency: {
decimals: 18,
name: 'Ritual',
symbol: 'RITUAL',
},
rpcUrls: {
default: {
http: ['https://rpc.ritualfoundation.org'],
webSocket: ['wss://rpc.ritualfoundation.org/ws'],
},
},
blockExplorers: {
default: {
name: 'Ritual Explorer',
url: 'https://explorer.ritualfoundation.org',
},
},
contracts: {
multicall3: {
address: '0x5577Ea679673Ec7508E9524100a188E7600202a3',
},
},
});
export const config = createConfig({
chains: [ritualChain],
connectors: [
injected(),
walletConnect({
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
}),
],
transports: {
[ritualChain.id]: http('https://rpc.ritualfoundation.org'),
},
});
import { getDefaultConfig } from '@rainbow-me/rainbowkit';
import { defineChain } from 'viem';
const ritualChain = defineChain({
id: 1979,
name: 'Ritual',
nativeCurrency: { decimals: 18, name: 'Ritual', symbol: 'RITUAL' },
rpcUrls: {
default: {
http: ['https://rpc.ritualfoundation.org'],
},
},
blockExplorers: {
default: {
name: 'Ritual Explorer',
url: 'https://explorer.ritualfoundation.org',
},
},
});
export const config = getDefaultConfig({
appName: 'My Ritual dApp',
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
chains: [ritualChain],
});
// app/providers.tsx
'use client';
import { WagmiProvider } from 'wagmi';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from './wagmi';
const queryClient = new QueryClient();
export function Providers({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}
[profile.default]
src = "src"
out = "out"
libs = ["lib"]
solc = "0.8.20"
optimizer = true
optimizer_runs = 200
[rpc_endpoints]
ritual = "${RITUAL_RPC_URL}"
[etherscan]
ritual = { key = "unused", url = "${RITUAL_VERIFIER_URL}" }
// script/Deploy.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Script, console} from "forge-std/Script.sol";
import {MyRitualConsumer} from "../src/MyRitualConsumer.sol";
contract DeployScript is Script {
function run() external {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
MyRitualConsumer consumer = new MyRitualConsumer();
console.log("MyRitualConsumer deployed to:", address(consumer));
vm.stopBroadcast();
}
}
# Load environment
source .env
# Deploy to Ritual Chain
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $RITUAL_RPC_URL \
--broadcast \
-vvvv
# Verify an already-deployed contract
forge verify-contract \
--chain 1979 \
--watch \
--verifier custom \
--verifier-url "$RITUAL_VERIFIER_URL" \
--verifier-api-key unused \
<CONTRACT_ADDRESS> \
src/MyRitualConsumer.sol:MyRitualConsumer
forge create (Single Contract)forge create deploys a single contract without needing a deploy script. This is useful for quick deployments and testing.
# Basic deployment
forge create src/MyRitualConsumer.sol:MyRitualConsumer \
--rpc-url $RITUAL_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast
# With constructor arguments
forge create src/MyRitualConsumer.sol:MyRitualConsumer \
--rpc-url $RITUAL_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--constructor-args 0x532F0dF0896F353d8C3DD8cc134e8129DA2a3948 100
# Deploy + verify in one command
forge create src/MyRitualConsumer.sol:MyRitualConsumer \
--rpc-url $RITUAL_RPC_URL \
--private-key $PRIVATE_KEY \
--broadcast \
--verify \
--verifier custom \
--verifier-url "$RITUAL_VERIFIER_URL" \
--verifier-api-key unused
Warning: Without
--broadcast,forge createonly simulates the deployment — the contract will NOT actually be deployed on-chain. Always include--broadcastfor real deployments.Verification note: Use
--verifier customwith the chain's verification service URL. Do NOT use Sourcify (chain 1979 is not registered). Do NOT point at the scanner UI hostname — use the RPC domain's/api/verify/path.
Ritual Chain (1979) uses a custom verification service accessible via the RPC domain. Sourcify does not support chain 1979. The scanner UI does not expose Etherscan/Blockscout-compatible API endpoints.
forge verify-contract \
--chain 1979 \
--watch \
--verifier custom \
--verifier-url "$RITUAL_VERIFIER_URL" \
--verifier-api-key unused \
<CONTRACT_ADDRESS> \
src/MyContract.sol:MyContract
Expected output sequence:
Submitting verification for [src/MyContract.sol:MyContract] 0x...Response: OK / GUID: <uuid>Pending in queue (may take ~15s)Details: Pass - VerifiedContract successfully verifiedFor contracts without constructor args:
forge create \
--broadcast \
--verify \
--verifier custom \
--verifier-url "$RITUAL_VERIFIER_URL" \
--verifier-api-key unused \
--rpc-url $RITUAL_RPC_URL \
--private-key $PRIVATE_KEY \
src/MyContract.sol:MyContract
For contracts with constructor args, prefer two-step: deploy first with forge create --broadcast, then verify separately with forge verify-contract. The combined one-liner can be brittle with complex constructor arg types.
| Symptom | Cause | Fix |
|---|---|---|
unsupported_chain | Used Sourcify (default verifier) | Add --verifier custom --verifier-url "$RITUAL_VERIFIER_URL" |
Bytecode does not match | Compiler/optimizer/EVM version mismatch | Check foundry.toml matches the settings used at deploy time |
Pending in queue for >2 min | Verifier service overloaded or stalled | Retry; check service health |
| HTML 404 response | Pointed verifier at scanner UI URL instead of RPC domain | Use https://rpc.ritualfoundation.org/api/verify not the scanner hostname |
# Create new Foundry project
forge init my-ritual-contracts
cd my-ritual-contracts
# Install OpenZeppelin (if needed)
forge install OpenZeppelin/openzeppelin-contracts
# Add remappings
echo '@openzeppelin/=lib/openzeppelin-contracts/' >> remappings.txt
import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
import 'dotenv/config';
const config: HardhatUserConfig = {
solidity: {
version: '0.8.20',
settings: {
optimizer: {
enabled: true,
runs: 200,
},
},
},
networks: {
ritual: {
url: process.env.RITUAL_RPC_URL || 'https://rpc.ritualfoundation.org',
chainId: 1979,
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
};
export default config;
// scripts/deploy.ts
import { ethers } from 'hardhat';
async function main() {
const [deployer] = await ethers.getSigners();
console.log('Deploying with:', deployer.address);
const balance = await ethers.provider.getBalance(deployer.address);
console.log('Balance:', ethers.formatEther(balance), 'RITUAL');
const Consumer = await ethers.getContractFactory('MyRitualConsumer');
const consumer = await Consumer.deploy();
await consumer.waitForDeployment();
console.log('MyRitualConsumer deployed to:', await consumer.getAddress());
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
npx hardhat run scripts/deploy.ts --network ritual
| Contract | Address |
|---|---|
| RitualWallet | 0x532F0dF0896F353d8C3DD8cc134e8129DA2a3948 |
| AsyncJobTracker | 0xC069FFCa0389f44eCA2C626e55491b0ab045AEF5 |
| TEEServiceRegistry | 0x9644e8562cE0Fe12b4deeC4163c064A8862Bf47F |
| Scheduler | 0x56e776BAE2DD60664b69Bd5F865F1180ffB7D58B |
| SecretsAccessControl | 0xf9BF1BC8A3e79B9EBeD0fa2Db70D0513fecE32FD |
| Contract | Address |
|---|---|
| SovereignAgentFactory | 0x9dC4C054e53bCc4Ce0A0Ff09E890A7a8e817f304 |
| PersistentAgentFactory | 0xD4AA9D55215dc8149Af57605e70921Ea16b73591 |
Verify before launch:
cast code "0x9dC4C054e53bCc4Ce0A0Ff09E890A7a8e817f304" --rpc-url "$RITUAL_RPC_URL"
cast code "0xD4AA9D55215dc8149Af57605e70921Ea16b73591" --rpc-url "$RITUAL_RPC_URL"
If either returns 0x, your deployment config is wrong for that RPC.
| Precompile | Short | Full Address |
|---|---|---|
| HTTP | 0x0801 | 0x0000000000000000000000000000000000000801 |
| LLM | 0x0802 | 0x0000000000000000000000000000000000000802 |
| Long HTTP | 0x0805 | 0x0000000000000000000000000000000000000805 |
| ZK | 0x0806 | 0x0000000000000000000000000000000000000806 |
| Image | 0x0818 | 0x0000000000000000000000000000000000000818 |
| Audio | 0x0819 | 0x0000000000000000000000000000000000000819 |
| Video | 0x081A | 0x000000000000000000000000000000000000081A |
| Precompile | Short | Full Address |
|---|---|---|
| ONNX | 0x0800 | 0x0000000000000000000000000000000000000800 |
| JQ | 0x0803 | 0x0000000000000000000000000000000000000803 |
| Ed25519 | 0x0009 | 0x0000000000000000000000000000000000000009 |
| SECP256R1 | 0x0100 | 0x0000000000000000000000000000000000000100 |
| Nitro | 0x0104 | 0x0000000000000000000000000000000000000104 |
| Contract | Address |
|---|---|
| Multicall3 | 0x5577Ea679673Ec7508E9524100a188E7600202a3 |
| WETH | 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2 |
| USDC | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
| Uniswap V3 Router | 0xE592427A0AEce92De3Edee1F18E0157C05861564 |
| Uniswap V3 Factory | 0x1F98431c8aD98523631AE4a59f267346ea31F984 |
Use these inline constants in your TypeScript code:
// Precompile addresses (async)
const PRECOMPILES = {
HTTP_CALL: '0x0000000000000000000000000000000000000801',
LLM: '0x0000000000000000000000000000000000000802',
LONG_RUNNING_HTTP: '0x0000000000000000000000000000000000000805',
ZK_TWO_PHASE: '0x0000000000000000000000000000000000000806',
IMAGE_CALL: '0x0000000000000000000000000000000000000818',
AUDIO_CALL: '0x0000000000000000000000000000000000000819',
VIDEO_CALL: '0x000000000000000000000000000000000000081A',
ONNX: '0x0000000000000000000000000000000000000800',
JQ: '0x0000000000000000000000000000000000000803',
} as const;
// Core system contracts
const SYSTEM_CONTRACTS = {
RITUAL_WALLET: '0x532F0dF0896F353d8C3DD8cc134e8129DA2a3948',
ASYNC_JOB_TRACKER: '0xC069FFCa0389f44eCA2C626e55491b0ab045AEF5',
TEE_SERVICE_REGISTRY: '0x9644e8562cE0Fe12b4deeC4163c064A8862Bf47F',
SCHEDULER: '0x56e776BAE2DD60664b69Bd5F865F1180ffB7D58B',
SECRETS_ACCESS_CONTROL: '0xf9BF1BC8A3e79B9EBeD0fa2Db70D0513fecE32FD',
} as const;
// Agent factory contracts
const AGENT_FACTORIES = {
SOVEREIGN_FACTORY: '0x9dC4C054e53bCc4Ce0A0Ff09E890A7a8e817f304',
PERSISTENT_FACTORY: '0xD4AA9D55215dc8149Af57605e70921Ea16b73591',
} as const;
For use in smart contracts:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
library RitualAddresses {
// Precompiles (async)
address constant HTTP_PRECOMPILE = address(0x0801);
address constant LLM_PRECOMPILE = address(0x0802);
address constant LONG_HTTP_PRECOMPILE = address(0x0805);
address constant ZK_PRECOMPILE = address(0x0806);
address constant IMAGE_PRECOMPILE = address(0x0818);
address constant AUDIO_PRECOMPILE = address(0x0819);
address constant VIDEO_PRECOMPILE = address(0x081A);
// Precompiles (native)
address constant ONNX_PRECOMPILE = address(0x0800);
address constant JQ_PRECOMPILE = address(0x0803);
address constant ED25519_PRECOMPILE = address(0x0009);
address constant SECP256R1_PRECOMPILE = address(0x0100);
address constant NITRO_PRECOMPILE = address(0x0104);
// System contracts
address constant RITUAL_WALLET = 0x532F0dF0896F353d8C3DD8cc134e8129DA2a3948;
address constant ASYNC_JOB_TRACKER = 0xC069FFCa0389f44eCA2C626e55491b0ab045AEF5;
address constant TEE_SERVICE_REGISTRY = 0x9644e8562cE0Fe12b4deeC4163c064A8862Bf47F;
address constant SCHEDULER = 0x56e776BAE2DD60664b69Bd5F865F1180ffB7D58B;
address constant SECRETS_ACCESS_CONTROL = 0xf9BF1BC8A3e79B9EBeD0fa2Db70D0513fecE32FD;
}
# ===== Chain Connection =====
RITUAL_RPC_URL=https://rpc.ritualfoundation.org
RITUAL_WS_URL=wss://rpc.ritualfoundation.org/ws
# ===== Deployer Account =====
PRIVATE_KEY=0x...your_deployer_private_key...
# ===== Contract Verification =====
RITUAL_VERIFIER_URL=https://rpc.ritualfoundation.org/api/verify
# ===== Frontend (public) =====
NEXT_PUBLIC_RITUAL_RPC_URL=https://rpc.ritualfoundation.org
NEXT_PUBLIC_WC_PROJECT_ID=your_walletconnect_project_id
# ===== Optional: Deployed Contract Addresses =====
NEXT_PUBLIC_CONSUMER_CONTRACT=0x...your_deployed_contract...
SOVEREIGN_FACTORY_ADDRESS=0x9dC4C054e53bCc4Ce0A0Ff09E890A7a8e817f304
PERSISTENT_FACTORY_ADDRESS=0xD4AA9D55215dc8149Af57605e70921Ea16b73591
# 1) Address has code
cast code "$SOVEREIGN_FACTORY_ADDRESS" --rpc-url "$RITUAL_RPC_URL"
cast code "$PERSISTENT_FACTORY_ADDRESS" --rpc-url "$RITUAL_RPC_URL"
# 2) Wiring sanity
cast call "$SOVEREIGN_FACTORY_ADDRESS" "scheduler()(address)" --rpc-url "$RITUAL_RPC_URL"
cast call "$SOVEREIGN_FACTORY_ADDRESS" "ritualWallet()(address)" --rpc-url "$RITUAL_RPC_URL"
cast call "$PERSISTENT_FACTORY_ADDRESS" "scheduler()(address)" --rpc-url "$RITUAL_RPC_URL"
cast call "$PERSISTENT_FACTORY_ADDRESS" "ritualWallet()(address)" --rpc-url "$RITUAL_RPC_URL"
Encrypted secrets require the dKMS service to be enabled and registered:
DKMS_ENABLED=true # Enable dKMS support in executor
TEE_SERVICE_REGISTRY_CONTRACT_ADDRESS=0x... # Registry for dKMS endpoint discovery
The executor discovers dKMS endpoints dynamically from TEEServiceRegistry. If DKMS_ENABLED is false (the default), encrypted secret operations will time out silently.
# Environment
.env
.env.local
.env.*.local
# Foundry
out/
cache/
broadcast/
# Hardhat
artifacts/
cache/
typechain-types/
# Dependencies
node_modules/
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
RITUAL_RPC_URL: string;
RITUAL_WS_URL: string;
PRIVATE_KEY: `0x${string}`;
RITUAL_VERIFIER_URL?: string;
NEXT_PUBLIC_RITUAL_RPC_URL: string;
NEXT_PUBLIC_WC_PROJECT_ID: string;
NEXT_PUBLIC_CONSUMER_CONTRACT?: `0x${string}`;
SOVEREIGN_FACTORY_ADDRESS?: `0x${string}`;
PERSISTENT_FACTORY_ADDRESS?: `0x${string}`;
}
}
Navigate to https://faucet.ritualfoundation.org, connect your wallet or paste your address, and request testnet RITUAL.
For headless/SSH environments, use the faucet API directly:
# Check faucet info (payout amount, rate limits)
curl https://faucet.ritualfoundation.org/api/info
# Claim testnet RITUAL
curl -X POST https://faucet.ritualfoundation.org/api/claim \
-H "Content-Type: application/json" \
-d '{"address": "0xYourAddressHere"}'
Rate-limited per address. For larger amounts needed for load testing, reach out via Ritual Discord.
For private/internal testnet deployments, the faucet runs on a deployment-specific endpoint. Consult your chain-deployment-infra configuration for the correct faucet service URL.
my-ritual-dapp/
├── contracts/ # Foundry project
│ ├── src/
│ │ └── MyConsumer.sol
│ ├── script/
│ │ └── Deploy.s.sol
│ ├── test/
│ │ └── MyConsumer.t.sol
│ ├── foundry.toml
│ └── .env
├── frontend/ # Next.js app
│ ├── app/
│ │ ├── layout.tsx
│ │ ├── page.tsx
│ │ └── providers.tsx
│ ├── lib/
│ │ ├── wagmi.ts
│ │ ├── ritual.ts # viem client setup
│ │ └── addresses.ts # Deployed contract addresses
│ ├── package.json
│ └── .env.local
├── backend/ # Optional API/indexer
│ ├── src/
│ │ └── index.ts
│ └── package.json
└── README.md
// frontend/lib/ritual.ts
import { createPublicClient, createWalletClient, http, defineChain } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const ritualChain = defineChain({
id: 1979,
name: 'Ritual',
nativeCurrency: { name: 'RITUAL', symbol: 'RITUAL', decimals: 18 },
rpcUrls: {
default: {
http: [process.env.NEXT_PUBLIC_RITUAL_RPC_URL || 'https://rpc.ritualfoundation.org'],
},
},
blockExplorers: {
default: { name: 'Ritual Explorer', url: 'https://explorer.ritualfoundation.org' },
},
});
// Read-only client for frontend (no private key needed)
let publicClientInstance: ReturnType<typeof createPublicClient> | null = null;
export function getPublicClient() {
if (!publicClientInstance) {
publicClientInstance = createPublicClient({ chain: ritualChain, transport: http() });
}
return publicClientInstance;
}
// For server-side operations that need signing
export function getSignerClients(privateKey: `0x${string}`) {
const account = privateKeyToAccount(privateKey);
return {
publicClient: createPublicClient({ chain: ritualChain, transport: http() }),
walletClient: createWalletClient({ account, chain: ritualChain, transport: http() }),
};
}
Step-by-step deployment workflow:
# 1. Compile contracts
cd contracts
forge build
# 2. Run tests locally
forge test -vvv
# 3. Deploy to Ritual
forge script script/Deploy.s.sol:DeployScript \
--rpc-url $RITUAL_RPC_URL \
--broadcast \
-vvvv
# 4. Note the deployed address from output
# MyRitualConsumer deployed to: 0x1234...
# 5. Verify on explorer
forge verify-contract \
--chain 1979 \
--watch \
--verifier custom \
--verifier-url "$RITUAL_VERIFIER_URL" \
--verifier-api-key unused \
0x1234... \
src/MyRitualConsumer.sol:MyRitualConsumer
# 6. Update frontend .env
echo 'NEXT_PUBLIC_CONSUMER_CONTRACT=0x1234...' >> ../frontend/.env.local
When the RPC endpoint is not directly reachable from the browser (internal testnet, IP-restricted, CORS issues), proxy RPC calls through your Next.js API route:
// app/api/rpc/route.ts
import { NextResponse } from 'next/server';
const RPC_URL = process.env.RITUAL_RPC_URL || 'https://rpc.ritualfoundation.org';
export async function POST(req: Request) {
const body = await req.text();
const resp = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
});
return new NextResponse(await resp.text(), {
headers: { 'Content-Type': 'application/json' },
});
}
Then configure your wagmi/viem transport to use /api/rpc instead of the direct RPC URL:
const ritualChain = defineChain({
id: 1979,
name: 'Ritual',
nativeCurrency: { name: 'RITUAL', symbol: 'RITUAL', decimals: 18 },
rpcUrls: {
default: { http: ['/api/rpc'] },
},
});
After deploying your dApp contracts, verify these items:
forge verify-contract --chain 1979 --watch --verifier custom --verifier-url "$RITUAL_VERIFIER_URL" --verifier-api-key unused <ADDRESS> <CONTRACT_ID> and confirm Pass - Verified100_000n blocks for development — 5000 blocks is ~29 min on the ~350ms conservative baseline; confirm with ritual-dapp-block-time)address(this))import { formatEther } from 'viem';
const RITUAL_WALLET = '0x532F0dF0896F353d8C3DD8cc134e8129DA2a3948' as const;
const RITUAL_WALLET_ABI = [
{ inputs: [{ name: 'user', type: 'address' }], name: 'balanceOf', outputs: [{ type: 'uint256' }], stateMutability: 'view', type: 'function' },
] as const;
const balance = await publicClient.readContract({
address: RITUAL_WALLET,
abi: RITUAL_WALLET_ABI,
functionName: 'balanceOf',
args: [deployerAddress],
});
console.log('RitualWallet balance:', formatEther(balance), 'RITUAL');
const TEE_SERVICE_REGISTRY = '0x9644e8562cE0Fe12b4deeC4163c064A8862Bf47F' as const;
const TEE_SERVICE_REGISTRY_ABI = [
{
inputs: [{ name: 'capability', type: 'uint8' }, { name: 'checkValidity', type: 'bool' }],
name: 'getServicesByCapability',
outputs: [{
type: 'tuple[]',
components: [
{ name: 'node', type: 'tuple', components: [
{ name: 'paymentAddress', type: 'address' },
{ name: 'teeAddress', type: 'address' },
{ name: 'teeType', type: 'uint8' },
{ name: 'publicKey', type: 'bytes' },
{ name: 'endpoint', type: 'string' },
{ name: 'certPubKeyHash', type: 'bytes32' },
{ name: 'capability', type: 'uint8' },
]},
{ name: 'isValid', type: 'bool' },
{ name: 'workloadId', type: 'bytes32' },
],
}],
stateMutability: 'view',
type: 'function',
},
] as const;
const HTTP_CALL_CAPABILITY = 0;
const services = await publicClient.readContract({
address: TEE_SERVICE_REGISTRY,
abi: TEE_SERVICE_REGISTRY_ABI,
functionName: 'getServicesByCapability',
args: [HTTP_CALL_CAPABILITY, true],
});
const validServices = services.filter((svc) => svc.isValid);
console.log(`Found ${services.length} HTTP services (${validServices.length} valid)`);
out/ or Hardhat artifacts/)Symptom: After redeploying contracts to a different chain or after a fresh deployment, the frontend shows empty state — no markets, no data, no errors. Contract calls silently revert because they target the old address which either does not exist or has different bytecode on the new chain.
Cause: Contract addresses were hardcoded directly in source files (e.g., const FACTORY = '0xabc...'). After redeployment, the address changes but the source code still points to the old one.
Solution: Always read deployed contract addresses from environment variables, with an optional fallback for local development:
// lib/addresses.ts
import { type Address } from 'viem';
// Always read from environment — never hardcode deployment-specific addresses
export const FACTORY_ADDRESS = (
process.env.NEXT_PUBLIC_FACTORY_ADDRESS ?? '0x0000000000000000000000000000000000000000'
) as Address;
export const MARKET_ADDRESS = (
process.env.NEXT_PUBLIC_MARKET_ADDRESS ?? '0x0000000000000000000000000000000000000000'
) as Address;
export const CONSUMER_ADDRESS = (
process.env.NEXT_PUBLIC_CONSUMER_CONTRACT ?? '0x0000000000000000000000000000000000000000'
) as Address;
Then in .env.local (frontend) or .env (backend):
NEXT_PUBLIC_FACTORY_ADDRESS=0x1234...actual_deployed_address
NEXT_PUBLIC_MARKET_ADDRESS=0x5678...actual_deployed_address
NEXT_PUBLIC_CONSUMER_CONTRACT=0xabcd...actual_deployed_address
Rules:
.env.local with the new addresses from the deploy script output and restart the frontend// lib/addresses.ts
if (!process.env.NEXT_PUBLIC_FACTORY_ADDRESS) {
console.warn(
'NEXT_PUBLIC_FACTORY_ADDRESS not set — contract reads will fail. ' +
'Run the deploy script and update .env.local with the new address.'
);
}
The TEEServiceRegistry has no services registered for the requested capability.
const HTTP_CALL_CAPABILITY = 0;
const services = await publicClient.readContract({
address: '0x9644e8562cE0Fe12b4deeC4163c064A8862Bf47F',
abi: TEE_SERVICE_REGISTRY_ABI, // defined above
functionName: 'getServicesByCapability',
args: [HTTP_CALL_CAPABILITY, true],
});
if (services.length === 0) {
console.error('No HTTP services available. Check:');
console.error('1. Are services registered on this network?');
console.error('2. Is the capability enum correct?');
}
Async calls fail if your RitualWallet deposit is too low or not locked.
import { parseEther } from 'viem';
const RITUAL_WALLET = '0x532F0dF0896F353d8C3DD8cc134e8129DA2a3948' as const;
const DEPOSIT_ABI = [
{ inputs: [{ name: 'lockDuration', type: 'uint256' }], name: 'deposit', outputs: [], stateMutability: 'payable', type: 'function' },
] as const;
// Deposit 5 RITUAL, locked for 500 blocks
const hash = await walletClient.writeContract({
address: RITUAL_WALLET,
abi: DEPOSIT_ABI,
functionName: 'deposit',
args: [5000n],
value: parseEther('5'),
});
await publicClient.waitForTransactionReceipt({ hash });
Users connecting from MetaMask may be on the wrong network.
import { useChainId, useSwitchChain } from 'wagmi';
function ChainGuard({ children }: { children: React.ReactNode }) {
const chainId = useChainId();
const { switchChain } = useSwitchChain();
if (chainId !== 1979) {
return (
<button onClick={() => switchChain({ chainId: 1979 })}>
Switch to Ritual Chain
</button>
);
}
return <>{children}</>;
}
Async precompile calls cannot be gas-estimated with standard estimateGas. Use explicit gas limits:
const HTTP_PRECOMPILE = '0x0000000000000000000000000000000000000801' as const;
// Encode the request using encodeAbiParameters (see ritual-dapp-overview for full encoding)
const hash = await walletClient.sendTransaction({
to: HTTP_PRECOMPILE,
data: encoded,
gas: 2_000_000n,
maxFeePerGas: 20_000_000_000n,
maxPriorityFeePerGas: 2_000_000_000n,
});
{
"dependencies": {
"@rainbow-me/rainbowkit": "^2.0.0",
"@tanstack/react-query": "^5.0.0",
"eciesjs": "^0.4.0",
"next": "^14.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"viem": "^2.0.0",
"wagmi": "^2.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"@types/react": "^18.0.0",
"typescript": "^5.0.0"
}
}
{
"dependencies": {
"viem": "^2.0.0",
"eciesjs": "^0.4.0",
"dotenv": "^16.0.0"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"tsx": "^4.0.0"
}
}
ritual-dapp-overview — Architecture overview, precompile categories, async lifecycleritual-dapp-contracts — Consumer contract patterns and Solidity templatesritual-dapp-frontend — React/Next.js frontend with async state machineritual-dapp-wallet — RitualWallet deposit, locking, and fee managementritual-dapp-testing — Testing and debugging Ritual dAppsnpx claudepluginhub ritual-foundation/ritual-dapp-skills --plugin ritual-dapp-skillsOutlines Ritual Chain architecture for dApp developers: 9-state async lifecycles, spcCalls receipts, TEE precompiles, Ethereum assumption corrections. Use starting new dApps or async patterns.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.