Web Wallets
- Location:
/e2e/e2e-wallets/ - Highlights: A comprehensive example frontend to use the
@effectstream/walletslibrary, demonstrating multi-chain wallet connections and different transaction submission methods.
The Wallets Demo is a developer tool and an interactive example that showcases how to integrate various blockchain wallets into a Effectstream dApp. It provides a clear, hands-on demonstration of connecting to different chains, signing messages, and submitting transactions, serving as a practical guide for developers.

Video Demo:
Core Concept: A Unified Wallet Experience
The primary goal of this demo is to illustrate the power and flexibility of the @effectstream/wallets library. It answers the question: "How can my dApp seamlessly handle wallets from different ecosystems like EVM, Cardano, Polkadot, and more?"
The application demonstrates three key user flows:
- Connecting Wallets: Shows how to initiate connections with a wide variety of wallets, from browser extensions like MetaMask to local, programmatic wallets for testing.
- Signing Messages: A standard way to verify ownership of an address without submitting an on-chain transaction.
- Submitting Transactions: Illustrates the different ways a user can send data to your Effectstream application:
- Direct On-Chain: A standard transaction sent to a specific Paima L2 contract.
- Via the Batcher: A gasless, user-friendly alternative where transactions are submitted through a Paima service.
This demo is an essential resource for any developer looking to build a multi-chain dApp on Effectstream.
How to Run
(TODO move to own template)
deno install --allow-scripts && ./patch.sh
deno task -f @e2e/evm-contracts build
deno task -f @e2e/evm-contracts deploy:standalone
deno task -f @e2e/midnight-contracts midnight-contract:compile
# If running on linux set env DISABLE_YACI=true
deno task -f @e2e/node dev
In another terminal, run the demo:
deno task -f @e2e/wallets-ui dev
The Components in Action
The demo is a standalone React + Vite application. Its interface is divided into three main sections:
- Select a Primitive: A "primitive" is a pre-configured listener for on-chain events. In this demo, you select which on-chain contract or event you want to interact with. The list is dynamically loaded from the engine's configuration.
- Connect Wallet: Based on the selected primitive, a list of compatible wallets is displayed. For example, selecting an EVM-based primitive will show EVM wallets like MetaMask and Phantom. This section allows you to connect and disconnect from your chosen wallet.
- Perform an Action: Once connected, you can interact with the application. For primitives linked to a Paima L2 contract, a form is dynamically generated based on the contract's
grammar, allowing you to send valid inputs.
Code Deep Dive: App.tsx
The entire logic is contained within /e2e/e2e-wallets/client/src/App.tsx. Let's break down the key parts.
1. Configuration
The application first sets up the necessary configuration to communicate with Effectstream and its related services.
-
paimaEngineConfig: This object tells the wallet library where to find key services, such as the Batcher and the Paima L2 contract. This normally is declared once per application globally.import { hardhat } from "viem/chains";
import { PaimaEngineConfig } from "@effectstream/wallets";
const paimaEngineConfig = new PaimaEngineConfig(
"my-app-name",
"parallelEvmRPC_fast", // paima l2 sync protocol name
"0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512", // paima l2 contract address
hardhat, // paima l2 chain
undefined, // use default paima l2 abi
"http://localhost:3334", // batcher url
);
2. Wallet Connection
The core of the wallet connection logic revolves around the walletLogin function from @effectstream/wallets. The demo pre-defines a list of connection options, each with a specific WalletMode.
// Example for an injected EVM wallet (MetaMask)
{
name: "EVM Injected",
mode: WalletMode.EvmInjected,
login: () =>
handleLogin(() =>
walletLogin({
mode: WalletMode.EvmInjected,
preference: { name: "io.metamask" },
})
),
types: ["evm"],
},
// Example for a programmatic local wallet for testing
{
name: "EVM (Viem Local Wallet)",
mode: WalletMode.EvmEthers,
login: () =>
handleLogin(() =>
walletLogin({
mode: WalletMode.EvmEthers,
connection: { /* ... custom ethers.js signer ... */ },
preferBatchedMode: false,
})
),
types: ["evm"],
},
The handleLogin wrapper function simply processes the result of the walletLogin call, updating the React state with the connected wallet's information or displaying an error.
3. Submitting a Concise Input to a Effectstream L2 Contract
When a user connects and selects a primitive corresponding to a Effectstream L2 contract, the application needs to show a form for the available functions. It does this by reading a grammar object, which defines the inputs for each state transition function.
This is a specific example on how to obtain the fields for a concise input for this template, but it's a general approach for any Effectstream L2 contract.
// A simplified view of the form rendering logic
const args = grammar[selectedFunction as keyof typeof grammar];
return (
<div>
<h3>{selectedFunction}</h3>
{args.map(([name, schema]) => (
<div key={name} className="form-field">
<label>{name}:</label>
{renderInput(name, schema, name)}
</div>
))}
{/* ... submit buttons ... */}
</div>
);
The handleSubmit function takes the user's input and uses the @effectstream/wallets library to send it to the blockchain. It showcases the three main interaction patterns you will normally use
- Signing a message
- Sending a direct transaction to the Effectstream L2 contract
- Sending a transaction via the Batcher
- Automatic Selection of the appropriate submission method based on the
preferBatchedModeflag
const handleSubmit = async () => {
const conciseData = [selectedFunction, ...Object.values(formValues)];
// Build message as ["action-name", "input-A", 100, ...]
switch (selectedAction) {
// Sign message
case "signMessage": {
// Verifies ownership without a transaction
const signedMessage = await signMessage(wallet, JSON.stringify(formValues));
setActionResult(JSON.stringify(signedMessage, null, 2));
break;
}
// Automatically selects the appropriate submission method based on the paimaEngineConfig
case "automatic": {
const result = await sendTransaction(
wallet,
conciseData,
paimaEngineConfig,
);
const str = JSON.stringify(result, null, 2);
setActionResult(`Transaction sent. Result: ${str}`);
break;
}
// Send Self Sequenced Transaction
case "sendSelfSequencedTransaction": {
// Submits a direct transaction to the Effectstream L2 contract
const result = await sendSelfSequencedTransaction(
wallet,
conciseData,
paimaEngineConfig,
);
const str = JSON.stringify(result, null, 2);
setActionResult(`Transaction sent. Result: ${str}`);
break;
}
// Submits the transaction via the gasless Batcher service
case "sendBatcherTransaction": {
const result = await sendBatcherTransaction(
wallet,
conciseData,
paimaEngineConfig,
);
const str = JSON.stringify(result, null, 2);
setActionResult(`Batcher transaction sent. Result: ${str}`);
break;
}
}
};
This demonstrates how a single wallet object, regardless of its underlying chain or type, can be used with a unified API (signMessage, sendTransaction, sendSelfSequencedTransaction, sendBatcherTransaction) to interact with the Effectstream ecosystem.