EVM-Compatible Chains
EVM (Ethereum Virtual Machine) chains serve as the primary layer for asset management and user interactions in many Effectstream dApps. Effectstream supports reading from RPC nodes and writing via standard or custom adapters.
1. Configuration (Read)
To read data from an EVM chain, you must configure the Network, the Sync Protocol, and the Primitives in your localhostConfig.ts.
Network Definition
Use addViemNetwork to easily import chain definitions from viem/chains.
.buildNetworks(builder =>
builder.addViemNetwork({
...hardhat, // or arbitrum, mainnet, polygon, etc.
name: "evmMain",
})
)
Sync Protocol
The protocol type is EVM_RPC_PARALLEL. This connects to a JSON-RPC endpoint.
.addParallel(
(networks) => networks.evmMain,
(network, deployments) => ({
name: "mainEvmRPC",
type: ConfigSyncProtocolType.EVM_RPC_PARALLEL,
chainUri: network.rpcUrls.default.http[0],
startBlockHeight: 1,
pollingInterval: 1000, // Polling frequency in ms
confirmationDepth: 1, // Number of blocks to wait for finality
}),
)
Primitives
Primitives define what events to listen for. Effectstream provides built-in primitives for common standards.
PrimitiveTypeEVMPaimaL2: Listens for thePaimaGameInteractionevent on thePaimaL2Contract.PrimitiveTypeEVMERC20: TracksTransferevents and maintains a dynamic table of token balances.PrimitiveTypeEVMERC721: Tracks NFTTransferevents and maintains ownership tables.PrimitiveTypeEVMERC1155: TracksTransferSingleandTransferBatchevents.
NOTE: A generic primitive example is available in the source code.
Example:
import { PrimitiveTypeEVMERC20 } from "@effectstream/sm/builtin";
.addPrimitive(
(syncProtocols) => syncProtocols.mainEvmRPC,
(network, deployments, syncProtocol) => ({
name: "MyToken",
type: PrimitiveTypeEVMERC20,
startBlockHeight: 0,
contractAddress: "0x...",
stateMachinePrefix: "erc20-transfer", // Triggers STF
})
)
2. Batcher Adapters (Write)
To write data to an EVM chain (e.g., submit game moves or mint tokens), you configure the Batcher with an Adapter.
Standard: Paima L2 Adapter
If you are using the PaimaL2Contract to submit generic game inputs, use the built-in PaimaL2DefaultAdapter.
import { PaimaL2DefaultAdapter } from "@effectstream/batcher";
const evmAdapter = new PaimaL2DefaultAdapter(
contractAddress,
privateKey,
fee,
"mainEvmRPC" // Matches your Sync Protocol name
);
Custom: Smart Contract Interaction
If you need to call specific functions with specific logic e.g., mint, swap, bridge only if the user has enough assets in their wallet, you can implement a custom BlockchainAdapter.
Example: A custom adapter that calls a transferToMidnight function on an ERC1155 contract (from the multi-chain-token-swap template).
export class ERC1155CustomAdapter implements BlockchainAdapter<string | null> {
// ... setup walletClient using viem ...
async submitBatch(data: string) {
// Parse the input data to determine arguments
const { args } = this.parseFunctionCall(data);
// Write to the contract using viem
const hash = await this.walletClient.writeContract({
address: this.erc1155Address,
abi: mct_erc1155.abi,
functionName: "transferToMidnight",
args: args,
});
return hash;
}
}
3. Browser Wallets (Connect)
In your frontend, you can use @effectstream/wallets to connect to EVM wallets (Metamask, Phantom, Rabby, etc.) using WalletMode.EvmInjected.
import { walletLogin, WalletMode } from "@effectstream/wallets";
const result = await walletLogin({
mode: WalletMode.EvmInjected,
// Optional: target specific wallet by RDNS or name
preference: { name: "io.metamask" },
preferBatchedMode: true, // Use batcher for transactions by default
});
if (result.success) {
const wallet = result.result;
console.log("Connected:", wallet.walletAddress);
}
4. Cryptography (Verify)
You can verify EVM signatures (EIP-191) inside your State Transition Functions or API routes using the CryptoManager.
Signing Messages
On the frontend, use the connected wallet to sign a message:
import { signMessage } from "@effectstream/wallets";
const signature = await signMessage(wallet, "Hello World");
Verifying Signatures
On the backend (Effectstream Node), verify the signature:
import { CryptoManager } from "@effectstream/crypto";
import { AddressType } from "@effectstream/utils";
const crypto = CryptoManager.getCryptoManager(AddressType.EVM);
// Verify an address string format (0x...)
const isValidAddress = crypto.verifyAddress("0x123...");
// Verify the signature
const isValidSig = await crypto.verifySignature(
userAddress,
"Hello World",
signature
);
5. Orchestration
Use launchEvm from @effectstream/orchestrator/start-evm to spin up a local Hardhat node and deploy contracts automatically.
// in start.ts
processesToLaunch: [
...launchEvm("@my-project/evm-contracts"),
]
NOTE: To use this launcher you need to implement some
deno taskin your project. A working implementation is provided in thetemplate generator,templatesore2e tests.
{
"name": "@chess/evm-contracts",
...
"tasks": {
"swap:remappings:forge": "deno -A @paimaexample/evm-hardhat/remappings-forge --depth 4 --package @paimaexample",
"swap:remappings:hardhat": "deno -A @paimaexample/evm-hardhat/remappings-hardhat",
"check": "echo 'check'",
"build:mod": {
// deploy:standalone does not return 'success', as the hardhat process is terminated by chain:stop - so it cannot be invoked as dependency
"command": "(deno task deploy:standalone || true) && deno run -A @paimaexample/evm-hardhat/builder"
},
"build:clean": "rm -rf build || true",
"build:forge": {
"command": "forge build",
"dependencies": [
"swap:remappings:forge"
]
},
"build:hardhat": {
"command": "deno run -A npm:hardhat@3.0.4 compile",
"dependencies": [
"swap:remappings:hardhat"
]
},
"build": {
"command": "deno task build:forge && deno task build:hardhat",
"dependencies": [
"build:clean"
]
},
"contract:compile": "deno task build:mod",
"deploy:clean": "rm -rf ignition/deployments || true",
"deploy:standalone": {
"command": "(((deno task chain:start) &) && (deno task chain:wait) && (deno task deploy) && deno task chain:stop) || true",
"dependencies": [
"build"
]
},
"deploy": {
"command": "deno run -A deploy.ts",
"dependencies": [
"deploy:clean"
]
},
"chain:start": "deno run -A npm:hardhat@3.0.4 node",
"chain:stop": "kill -9 $(lsof -ti tcp:8545) || true",
"chain:wait": "deno run -A npm:hardhat@3.0.4 node wait"
}
}