EVM
Overview
EVM settlement layer is a settlement layer that uses blockchain technology under the hood with the following curated tech stack for optimal performance:
- Ethereum Virtual Machine (EVM) - blockchains compatible with Ethereum VM for smart contract execution
- Hardhat - for smart contract development and deployment
- Viem - for low-level blockchain interactions
- Wagmi - for React hooks and wallet connection
- Web3Auth - for enhanced user experience with Account Abstraction
Development
Smart Contracts
EVM settlement layer uses Hardhat V3 for smart contract development. Your contracts are located in the packages/evm/contracts directory.
Want to learn more?
Checkout the Hardhat docs to see how to configure your development environment.
Fundset is using a Diamond Proxy pattern for the main smart contract with logic. This allows for easy and composable upgrades and extensions of the main contract - See arguments for and against using Diamond Proxy pattern. Of course you can create separate contracts for each module if you need, e.g. an ERC20 token.
Context Provider
_fundset/settlement-layer/evm/index.tsx file is a context provider component that wraps your app with Wagmi configuration and chain management:
const EvmSettlementLayerProvider = ({
children,
...props
}: React.PropsWithChildren<{ config: EvmSettlementLayerConfig }>) => {
const { wagmiConfig } = useMemo(() => buildEvmSettlementLayer(props.config), [props.config]);
useEffect(function hotContractReload() {
if (process.env.NODE_ENV === 'development') {
const wss = new WebSocket('http://localhost:9999');
wss.onopen = () => {
console.log('Hot Contract Reload: WebSocket connection established');
};
wss.onmessage = event => {
if (event.data === 'reload') {
window.location.reload();
}
};
return () => {
console.log('Hot Contract Reload: WebSocket connection closed');
wss.close();
};
}
}, []);
return (
<EvmChainConfigsProvider config={props.config.chainConfigs}>
<Web3AuthProvider config={{ web3AuthOptions }}>
<WagmiProvider config={wagmiConfig}>
<SimpleKitProvider>{children}</SimpleKitProvider>
</WagmiProvider>
</Web3AuthProvider>
</EvmChainConfigsProvider>
);
};
export default EvmSettlementLayerProvider;Modules
Each module that supports EVM, will have a evm directory inside it.
Check out the docs on how to build your own modules.
Payload CMS Plugin
_fundset/settlement-layer/evm/plugin folder contains the Payload CMS plugin files for managing EVM settlement layer configuration:
Check out the Payload docs to learn more about plugins.
Account Abstracttion
_fundset/settlement-layer/evm/connectors folder contains web3auth wallet connection implementation. In order for it to work, you have to generate an API key for web3auth.
First file is the wrapper around WagmiConfig component that synchronises wagmi with web3auth connection: _fundset/settlement-layer/evm/connectors/WagmiProviderWithAA.tsx.
Second is the builder of the smart account client: _fundset/settlement-layer/evm/connectors/getSmartAccountClient.ts. This function is responsible for building the smart account viem client. The underlying smart account is Kernel by ZeroDev.
The AA standard used is ERC-4337 with all bundlers supported and Pimlico paymaster. This stack is of course fully editable as you have full control over the implementation so you can bring your own AA providers. For example, You can easily switch to ERC-7677 paymaster by using createPaymasterClient from viem/account-abstraction package if you don't want to use Pimlico.
Indexer
Fundset uses Envio HyperIndex for indexing blockchain events. It is availible under packages/evm/indexer folder.
Each module might need to index events from the blockchain so in order to add a new module with indexing logic, you need to create a new file in packages/evm/indexer/src/modules folder and then, the postinstall script will automatically generate an import to your module's indexing logic.
Envio indexer requires a yaml config file to know which events on which contracts to index. By default, the dev deployment script generates a config with all the events on all the contracts that are returned from the deployment script.
But you have to add the logic for indexing events in the packages/evm/indexer/src/EventHandlers.ts file by yourself. Check out the Event Handlers docs to learn how to do it.
Querying the Indexer from Frontend
Fundset uses gql.tada for type-safe GraphQL queries and graphql-request to communicate with the Envio indexer.
Setup
The GraphQL schema is configured in tsconfig.json:
{
"compilerOptions": {
"plugins": [
{
"name": "gql.tada/ts-plugin",
"schema": "http://localhost:8080/v1/graphql",
"tadaOutputLocation": "./src/graphql-env.d.ts"
}
]
}
}To generate TypeScript types from your indexer's GraphQL schema, run:
pnpm generate:schemaThis generates graphql-env.d.ts with full type definitions from your indexer.
Writing Queries
Use gql.tada to write type-safe GraphQL queries. The graphql function provides full IntelliSense and type checking:
import { graphql } from 'gql.tada';
import { request } from 'graphql-request';
import { evmSettlementLayerEnv } from '../../env';
globalCounterIncrementEventsQueryOptions: ({ limit, offset }) => ({
queryKey: [
'global-counter-increment-events',
evmModule.proxyAddress,
chain.id,
limit,
offset,
],
queryFn: async () => {
const globalCounterIncrementEventsQuery = graphql(`
query GlobalCounterIncrementEvents($limit: Int!, $offset: Int!) {
Counter_IncrementBy(limit: $limit, offset: $offset) {
amount
by
id
timestamp
}
}
`);
const globalCounterIncrementEvents = await request(
evmSettlementLayerEnv().NEXT_PUBLIC_INDEXER_URL,
globalCounterIncrementEventsQuery,
{ limit, offset },
);
return globalCounterIncrementEvents.Counter_IncrementBy.map(event => ({
...event,
timestamp: new Date(Number(event.timestamp)),
}));
},
}),The query names (like Counter_IncrementBy) are automatically generated from your indexer schema and correspond to the entities/events you defined.
Using in React Components
Query options returned from modules integrate seamlessly with TanStack Query:
import { useQuery, keepPreviousData } from '@tanstack/react-query';
import { useSettlementLayer } from '@/_fundset/settlement-layer';
export const IncrementHistory = () => {
const {
counter: { globalCounterIncrementEventsQueryOptions },
} = useSettlementLayer();
const [offset, setOffset] = useState(0);
const limit = 3;
const {
data: events = [],
isLoading,
isFetching,
} = useQuery({
...globalCounterIncrementEventsQueryOptions({ limit, offset }),
placeholderData: keepPreviousData,
});
return (
<div>
{events.map(event => (
<div key={event.id}>
<p>Incremented by: {event.by}</p>
<p>New amount: {event.amount}</p>
<p>Time: {event.timestamp.toLocaleString()}</p>
</div>
))}
</div>
);
};Waiting for Indexing
After submitting a transaction, you may want to wait for it to be indexed before refetching queries. Use the waitForTransactionToBeIndexed helper:
import { waitForTransactionToBeIndexed } from '../../helpers';
incrementGlobalCounterMutationOptions: () => ({
mutationFn: async (amount: number) => {
const hash = await writeContract(config, {
// ... contract write options
});
await waitForTransactionReceipt(config, { hash });
// Wait for indexer to process the transaction
await waitForTransactionToBeIndexed(hash);
},
meta: {
// Invalidate queries after successful mutation
invalidatesQueries: [
['global-counter-increment-events', evmModule.proxyAddress, chain.id],
],
},
}),The waitForTransactionToBeIndexed function polls the indexer's raw_events table to check if your transaction has been processed.
Best Practices
- Query Keys: Include all variables that affect the query result in your query key (contract address, chain ID, pagination params)
- Type Safety: Let
gql.tadainfer types - avoid manual type annotations - Data Transformation: Transform indexer data (like converting timestamps to Date objects) in the
queryFn - Query Invalidation: Use
meta.invalidatesQueriesto automatically refetch data after mutations - Environment Variables: Store the indexer URL in
NEXT_PUBLIC_INDEXER_URLenvironment variable
Deployment
For EVM settlement layer to work, you need:
- Smart Contracts Deployed - Deploy your contracts to your target networks using Hardhat
- RPC Endpoints - Configure RPC URLs for each chain you want to support
- Web3Auth Configuration for Account Abstraction (Optional) - Set up Web3Auth for wallet authentication and configure bundler and paymaster (e.g. Biconomy)
1. Contract Deployment
Fundset recomments using Hardhat Ignition for contract deployment.
- First configure your target chain in
hardhat.config.ts:
const config: HardhatUserConfig = {
plugins: [hardhatToolboxViemPlugin],
profiles: {
default: {
version: '0.8.28',
settings: {
viaIR: true,
optimizer: {
enabled: true,
runs: 200,
},
},
},
},
networks: {
mainnet: {
type: 'http',
chainType: 'l1',
url: configVariable('MAINNET_RPC_URL'),
accounts: [configVariable('MAINNET_PRIVATE_KEY')],
},
},
};-
Set configuration variables in your hardhat keystore. In this example, we'll need MAINNET_RPC_URL and MAINNET_PRIVATE_KEY.
-
Then check out the
deploy/production.tsscript and edit it to deploy your contracts if you added any. Remember to edit a network name to match your target chain in thenetwork.connectcall.
// ... imports
export const deploy = async (networkConnection: NetworkConnection, deployer: Deployer) => {
const { ignition } = networkConnection;
const { diamondProxy } = await ignition.deploy(DiamondProxyModule, {
defaultSender: deployer.account.address,
displayUi: true,
});
const { counterV1, counterV2 } = await ignition.deploy(CounterVersionsModule, {
defaultSender: deployer.account.address,
displayUi: true,
});
const { erc20 } = await ignition.deploy(ERC20Module, {
defaultSender: deployer.account.address,
displayUi: true,
});
return {
DiamondProxy: {
contract: diamondProxy,
facets: {
CounterV1: counterV1,
CounterV2: counterV2,
},
},
ERC20: {
contract: erc20,
},
};
};
const isDirect =
import.meta.url === `file://${process.argv[1]}` ||
process.argv[1] === path.resolve(import.meta.dirname, '../node_modules/hardhat/dist/src/cli.js');
if (isDirect) {
const networkConnection = await network.connect({ network: 'mainnet' }); // [!Code highlight]
const [deployer] = await networkConnection.viem.getWalletClients();
deploy(networkConnection, deployer).catch(console.error);
}Return format
We suggest following the return format of [ContractName] having contract property and optional
facets property so it can be properly parsed by the helper function that we use in development.
-
Then run
pnpm hardhat run deploy/production.tsto actually deploy your contracts. Check out the Hardhat Ignition deployments docs to learn more about how we use Hardhat Scripts with Ignition. -
Copy the addresses of deployed contracts, you'll add them in the payload CMS admin dashboard later.
Deployed Addresses
DiamondProxyModule#DiamondProxy - 0x6334d9772bb59c1d4f2B4e2fFbd8E1a010cB0295
CounterVersionsModule#CounterV1 - 0x4FB376a84dDAcB75d2978679A969E76E437Bfa30
CounterVersionsModule#CounterV2 - 0x5A065b9C476fe87308eE91Ac47f63bfBeeD618Fb
ERC20Module#ERC20 - 0x3c403B2DACeDedcfa5cDdE20D2b464395F3edE182. Application deployment
This is a step by step guide on how to deploy an app with the evm settlement layer to Vercel.
- Publish your fundset app's code to github
- Create a postgres database instance e.g. with Neon on Vercel
- set the database connection string in the
DATABASE_URIenvironment variable in your .env file - in the
packages/webfolder, create Payload migrations withpnpm payload migrate:create - run Payload migrations with
pnpm payload migrate - Create a new Vercel project out of it and add the following environment variables to your Vercel project:
DATABASE_URI- your postgres database connection stringPAYLOAD_SECRET- a random stringNEXT_PUBLIC_WEB3AUTH_CLIENT_ID- your web3auth client id (optional, needed for AA)NEXT_PUBLIC_APP_URL- URL of your fundset app (e.g.https://<your-app-name>.vercel.app)NEXT_PUBLIC_INDEXER_URL- URL of your indexer (e.g.http://localhost:8080/v1/graphql)
- Deploy your Vercel project.
- Go to
/adminpage and create a new admin user and log in. - In the admin dashboard, go to
Fundset Settlement LayerGlobal and add a settlement layer config for EVM with RPC URLs for each supported chain and modules with contract addresses that you deployed before.
Your app is now ready to use!