A production-ready React dApp demonstrating LLM ERC20 token interactions on GenLayer StudioNet using GenLayerJS SDK with Material UI.
# Install dependencies
pnpm i
# Setup environment
cp .env.example .env
# Edit .env with your contract address and private key
# Start development server
pnpm devRequired Environment Variables:
VITE_CONTRACT_LLM_ERC20=<0x...>- Your deployed contract addressVITE_PK=<private key test>- Test private key for direct signingVITE_ADDR=<address>- Alternative: address for external signing (MetaMask)
A mini dApp that reads/writes to llm_erc20 contract with the following features:
- Read Operations:
get_balance_of,get_balances - Write Operations:
mint,transfer - Modern UI: Material UI with dark/light theme toggle
- Real-time Feedback: Snackbar notifications and loading states
GenLayer Studio is the IDE/Console for deploying Intelligent Contracts - smart contracts enhanced with AI capabilities for complex decision-making.
The official JavaScript SDK for interacting with GenLayer network:
- Create clients for different chains (studionet, testnetAsimov)
- Read/write contract data
- Wait for transaction confirmations
- Retrieve contract schemas
- Optimistic Democracy Consensus: Transactions are optimistically accepted and can be appealed if consensus is challenged
- Equivalence Principle: All participants have equal voting power in the consensus mechanism
- Open studio.genlayer.com
- Click "New Contract" → Paste
llm_erc20contract code - Deploy on StudioNet
- Copy the Contract Address
import { createClient, createAccount } from 'genlayer-js';
import { studionet } from 'genlayer-js/chains';
import { TransactionStatus } from 'genlayer-js/types';
// Setup client with studionet
const account = createAccount(process.env.PK!);
const client = createClient({ chain: studionet, account });
// Initialize consensus (required before any operations)
await client.initializeConsensusSmartContract();
// Deploy contract
const hash = await client.deployContract({
code: CONTRACT_CODE,
args: [],
leaderOnly: false
});
// Wait for deployment confirmation
const receipt = await client.waitForTransactionReceipt({
hash,
status: TransactionStatus.ACCEPTED,
retries: 50,
interval: 5000,
});
console.log('Deployed at:', receipt.data?.contract_address);
⚠️ Security Warning: Only use test private keys for development. Never use production keys.
- Deploy contract on GenLayer Studio (StudioNet) → copy contract address
- Mint test tokens in Studio (or via dApp after configuring keys)
- Configure .env:
VITE_CONTRACT_LLM_ERC20, choose one signing method:VITE_PK(test private key) orVITE_ADDR(MetaMask) - Run dApp:
pnpm i && pnpm dev - On-chain interaction from UI:
get_balance_of→get_balances→transfer - Log & screenshot: read results, transfer accepted
- Submit mission: repo link + images/short screencast
- Pending transactions: Use
waitForTransactionReceiptwithACCEPTEDstatus, increase retries/interval - Network delays: StudioNet may have higher latency than localnet
- Consensus time: Transactions may take longer to reach finality
Create .env file:
VITE_CONTRACT_LLM_ERC20=0xYourContractOnStudionet
# Choose ONE of the following
VITE_PK=0xYourTestPrivateKey
# VITE_ADDR=0xYourAddressForExternalSigning
# (optional) VITE_RPC_ENDPOINT=https://studio.genlayer.com/api
⚠️ Security Warning: Only use test private keys for development. Never use production keys.
Explanation:
VITE_PK: SDK handles signing directly with private keyVITE_ADDR: External signing via wallet (MetaMask)
pnpm add @mui/material @mui/icons-material @emotion/react @emotion/styledThe project uses Material UI with dark theme by default and theme toggle functionality:
import { CustomThemeProvider } from './contexts/ThemeContext'
import ThemeWrapper from './components/ThemeWrapper'
const AppWithTheme = () => {
return (
<CustomThemeProvider>
<ThemeWrapper>
<App />
</ThemeWrapper>
</CustomThemeProvider>
);
};The Erc20Demo.tsx component utilizes these MUI components:
- AppBar: Fixed header with title and theme toggle
- Card: Main content containers
- Grid: Responsive layout system
- Table: Display all balances data
- Snackbar: Real-time notifications
- TextField: Input fields with validation
- Button: Action buttons with loading states
import { createClient, createAccount } from "genlayer-js";
import { studionet } from "genlayer-js/chains";
import { TransactionStatus } from "genlayer-js/types";
const CONTRACT = import.meta.env.VITE_CONTRACT_LLM_ERC20 as `0x${string}`;
const PK = import.meta.env.VITE_PK as string | undefined;
const ADDR = import.meta.env.VITE_ADDR as `0x${string}` | undefined;
// Create client with proper configuration
let clientConfig: any = { chain: studionet };
if (PK) {
clientConfig.account = createAccount(PK as `0x${string}`);
} else if (ADDR) {
clientConfig.account = ADDR;
}
export const client = createClient(clientConfig);
// Initialize consensus smart contract (required before any contract interaction)
let initPromise: Promise<void> | null = null;
export async function ensureInitialized() {
if (!initPromise) {
initPromise = client.initializeConsensusSmartContract();
}
return initPromise;
}// Low-level contract interaction wrappers
export async function readContract(functionName: string, args: any[] = []) {
await ensureInitialized();
return client.readContract({
address: CONTRACT,
functionName,
args,
});
}
export async function writeContract(functionName: string, args: any[] = []) {
await ensureInitialized();
const hash = await client.writeContract({
address: CONTRACT,
functionName,
args,
value: 0n,
});
// Wait for transaction to be accepted
return client.waitForTransactionReceipt({
hash,
status: TransactionStatus.ACCEPTED,
retries: 60,
interval: 1000,
});
}import { erc20, getSchema } from "../../lib/genlayer";
export async function getBalanceOf(address: string) {
if (!/^0x[a-fA-F0-9]{40}$/.test(address)) throw new Error("Invalid address");
const v = await erc20.getBalanceOf(address);
return Number(v);
}
export async function getBalances() {
try {
console.log("Fetching balances...");
const rawData = await erc20.getBalances();
console.log("Raw balances data:", rawData, "Type:", typeof rawData);
// Handle Map object response
if (rawData instanceof Map) {
const result = Array.from(rawData.entries()).map(([address, balance]) => ({
address,
balance: Number(balance)
}));
console.log("Map balances:", result);
return result;
}
// Handle other formats...
return [];
} catch (error) {
console.error("Error fetching balances:", error);
return [];
}
}export default function Erc20Demo() {
const { mode, toggleMode } = useCustomTheme();
const [connectedWallet, setConnectedWallet] = useState<string>('');
const [walletBalance, setWalletBalance] = useState<string | number>('-');
// Auto-load wallet balance when wallet is connected
useEffect(() => {
if (connectedWallet) {
loadWalletBalance();
}
}, [connectedWallet]);
const handleTransfer = async () => {
const result = await safe(() => transfer(Number(transferAmount), transferTo));
if (result !== undefined) {
showSnackbar('Transfer completed successfully!', 'success');
await loadWalletBalance(); // Refresh balance
}
};
}// Initialize client and consensus
const client = createClient({ chain: studionet, account });
await client.initializeConsensusSmartContract();
// Get balance of specific address
const balance = await client.readContract({
address: CONTRACT,
functionName: 'get_balance_of',
args: ['0x584713626396fA15CA12870d924f743CD1c09961'],
});
// Get all balances
const allBalances = await client.readContract({
address: CONTRACT,
functionName: 'get_balances',
args: [],
});// Initialize client and consensus
const client = createClient({ chain: studionet, account });
await client.initializeConsensusSmartContract();
// Transfer tokens
const hash = await client.writeContract({
address: CONTRACT,
functionName: 'transfer',
args: [100, '0xReceiver'],
value: 0n,
});
// Wait for confirmation
const receipt = await client.waitForTransactionReceipt({
hash,
status: TransactionStatus.ACCEPTED,
retries: 60,
interval: 1000,
});// Initialize client and consensus
const client = createClient({ chain: studionet, account });
await client.initializeConsensusSmartContract();
// Get contract schema
const schema = await client.getContractSchema({ address: CONTRACT });
console.log(schema.methods);# Install dependencies
pnpm i
# Start development server
pnpm dev- Check Balance: Enter address → Click "Get Balance"
- View All Balances: Click "Get All Balances" → See table with all addresses
- Transfer Tokens: Enter receiver address and amount → Click "Transfer Tokens"
- Theme Toggle: Switch between dark/light mode
- Copy Addresses: Click copy icons to copy contract/addresses
- Real-time Feedback: Snackbar notifications for success/error
- Loading States: Circular progress indicators during operations
- Address Validation: Automatic validation for Ethereum addresses
- Responsive Design: Mobile-friendly layout
- Accessibility: ARIA labels and keyboard navigation
InternalRpcError: Invalid parameters
- Check address format (0x + 40 hex characters)
- Ensure amount > 0
- Verify contract address is correct
Wallet not connected: Missing authentication
- Add
VITE_PKfor direct signing - Or add
VITE_ADDRfor external signing - Ensure private key is valid
Transaction pending: Long wait times
- Use
waitForTransactionReceiptwith proper retry settings - Check network status
- Try again with higher retry count
CORS errors: Custom endpoint issues
- Use default studionet endpoint
- Or configure dev proxy in vite.config.ts
// Enable debug logging
console.log("Raw data:", rawData);
console.log("Transaction hash:", hash);
console.log("Receipt:", receipt);- Deploy
llm_erc20on StudioNet - Get
contract addressand configure.env - Demo UI: read / write operations working
- Screenshots: get_balance_of, get_balances, transfer accepted
- README with complete guide + GenLayerJS code examples
- (Optional) 1-2 minute demo video
- Add
log_indexeras second module - Implement
appealTransactionfor consensus cases - Create React hook
useGenLayerClient()for shared client and tx state - Add batch transaction processing
- Implement real-time event listening
- Multi-signature wallet integration
- Contract upgrade mechanisms
- Advanced error handling and retry logic
- Performance optimization for large datasets
- Integration with external APIs and services
# Contract Configuration
VITE_CONTRACT_LLM_ERC20=0xYourContractOnStudionet
# Choose ONE of the following
VITE_PK=0xYourTestPrivateKey
# VITE_ADDR=0xYourAddressForExternalSigning
# (optional) VITE_RPC_ENDPOINT=https://studio.genlayer.com/api
⚠️ Security Warning: Only use test private keys for development. Never use production keys.
{
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
}
}MIT License - see LICENSE file for details.
Built with ❤️ using GenLayerJS and Material UI