A comprehensive, maintainable Web3 development framework that solves common pitfalls in dApp development.
SmartDapp is a TypeScript library designed to provide a clean, structured approach to Web3 development. Born from real-world experience building decentralized applications, it addresses the common issues that plague dApp development teams - particularly the chaos that ensues when developers lack proper Web3 and API communication patterns.
During the development of decentralized applications, we identified critical patterns that lead to unmaintainable codebases. The core issues were:
- Misunderstanding of Web3 patterns: Improper handling of wallet connections, network switching, and contract interactions
- Poor API communication: Lack of structured patterns for handling different networks and environments
- Inconsistent state management: No clear separation between on-chain and off-chain data
- Getter abuse: Developers relying on non-reactive getters instead of proper state management
- Maintenance challenges: Code that works but becomes difficult to understand, debug, or extend over time
SmartDapp was created to solve these problems by providing:
- Event-driven architecture: Forces developers to use reactive patterns instead of getters
- Structured Web3 interactions with proper error handling
- Network-aware configuration that scales across environments
- Type-safe contract interactions with clear patterns
- Maintainable architecture that teams can actually work with
- Best practices built-in to prevent common Web3 mistakes
- No Getters: Removes all getter methods to prevent non-reactive state access
- Event-Only Updates: All state changes are communicated through events
- Reactive Patterns: Forces developers to implement proper state management
- Predictable State: Clear event flow makes debugging and maintenance easier
- Wallet Management: Seamless connection with AppKit integration
- Network Switching: Automatic network detection and switching
- Multi-chain Support: Configure multiple networks with different contracts
- Event Subscriptions: React to wallet and network changes
- ABI Organization: Centralized ABI management with type safety
- Contract Registry: Named contracts with network-specific addresses
- Transaction Handling: Gas estimation and proper transaction flow
- Read Operations: Optimized read calls with provider management
- Network-aware Storage: Data automatically namespaced by network
- Metadata Management: Store and retrieve application-specific data
- Search Capabilities: Find stored data by any field
- Cache Management: Built-in caching with network-specific keys
- Environment-aware URLs: Different API endpoints per network
- Type-safe Configuration: Compile-time validation of API structures
- Flexible Backend Integration: Support for any backend architecture
TODO:not published on npm yetimport { SmartDapp, SmartDappConfig } from 'smartdapp';
const config: SmartDappConfig = {
appName: "My DApp",
appDescription: "A decentralized application",
appUrl: "https://mydapp.com",
appIcon: "/logo.png",
// Define your ABIs
abis: {
"ERC20": [
// ERC20 ABI here
],
"UniswapV2Router": [
// Router ABI here
]
},
// Configure networks
networks: {
1: { // Ethereum Mainnet
appKit: {
id: 1,
name: "Ethereum",
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["https://eth.llamarpc.com"] } },
blockExplorers: { default: { url: "https://etherscan.io" } }
},
customNetworkId: "ethereum-mainnet",
contracts: {
"Router": {
name: "UniswapV2Router",
address: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"
}
},
metadata: {
"weth9": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"defaultRefCode": "REF123"
}
},
137: { // Polygon
appKit: {
id: 137,
name: "Polygon",
nativeCurrency: { name: "MATIC", symbol: "MATIC", decimals: 18 },
rpcUrls: { default: { http: ["https://polygon.llamarpc.com"] } },
blockExplorers: { default: { url: "https://polygonscan.com" } }
},
customNetworkId: "polygon-mainnet",
contracts: {
"Router": {
name: "UniswapV2Router",
address: "0xa5E0829CaCEd8fFDD4De3c43696c57F7D7A678ff"
}
},
metadata: {
"weth9": "0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270",
"defaultRefCode": "REF456"
}
}
},
// Configure API URLs per network
apiUrls: {
1: {
"markets": { url: "https://markets.mydapp.com" },
"backend": { url: "https://api.mydapp.com" },
"analytics": { url: "https://analytics.mydapp.com" }
},
137: {
"markets": { url: "https://markets-polygon.mydapp.com" },
"backend": { url: "https://api-polygon.mydapp.com" },
"analytics": { url: "https://analytics-polygon.mydapp.com" }
}
}
};
// Initialize SmartDapp
const smartDapp = new SmartDapp(config, false); // false = production mode// Subscribe to wallet changes FIRST - this is the only way to get state updates
smartDapp.subscribeToChanges((event) => {
switch (event.event) {
case "CONNECTED":
console.log("Wallet connected to:", event.properties.address);
// Handle connection state in your UI
break;
case "DISCONNECTED":
console.log("Wallet disconnected:", event.properties.reason);
// Handle disconnection state in your UI
break;
case "NETWORK_CHANGED":
console.log("Network changed to:", event.properties.chainId);
console.log("API URLs for this network:", event.properties.apiUrls);
// Handle network change in your UI
// event.properties.apiUrls contains all configured API endpoints for the new network
break;
}
});
// Open wallet connection modal
await smartDapp.openAppKitModal();
// Note: There are NO getters - you must rely on events for state updates// Read from contract
const balance = await smartDapp.readCall("Router", "getAmountsOut", [
"1000000000000000000", // 1 ETH
["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xA0b86a33E6441b8c4C8C0C8C0C8C0C8C0C8C0C8C"] // WETH -> Token
]);
// Send transaction
const tx = await smartDapp.sendTransaction("Router", "swapExactETHForTokens", [
"0", // min amount out
["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", "0xA0b86a33E6441b8c4C8C0C8C0C8C0C8C0C8C0C8C"], // path
"0x123...", // recipient
Math.floor(Date.now() / 1000) + 1800 // deadline
], "1000000000000000000"); // 1 ETH value
console.log("Transaction hash:", tx.hash);// Store network-specific data
smartDapp.storeMetadata("storedTokens", [
{
address: "0xA0b86a33E6441b8c4C8C0C8C0C8C0C8C0C8C0C8C",
symbol: "USDC",
name: "USD Coin",
decimals: 6
}
]);
// Find token by any field
const token = await smartDapp.findContentByKeyAndQuery("storedTokens", "USDC");
console.log("Found token:", token);
// Note: API URLs are now only available through NETWORK_CHANGED events
// See the event-driven examples above for how to access themSmartDapp enforces an event-driven architecture by removing all getter methods. This design decision addresses a critical problem in Web3 development: developers often rely on non-reactive getters instead of implementing proper state management.
Traditional Web3 libraries provide getters like getAddress() or getNetworkId(), but these create several problems:
- Non-reactive: Getters don't automatically update when state changes
- Stale data: Developers forget to re-fetch data after state changes
- Poor UX: UI doesn't update when wallet disconnects or network changes
- Debugging nightmare: Hard to track when and why state becomes inconsistent
Instead of getters, SmartDapp provides:
- Event subscriptions:
subscribeToChanges()is the only way to get state updates - Reactive patterns: Forces developers to implement proper state management
- Predictable flow: Clear event flow makes debugging easier
- Better UX: UI automatically updates when state changes
- Network-aware data: Events include relevant data like API URLs for the current network
- Type safety: Discriminated union types provide compile-time validation and IntelliSense
SmartDapp uses discriminated union types for type-safe event handling:
export type SmartDappEvent = {
event: 'CONNECTED';
properties: {
address: string;
};
} | {
event: 'DISCONNECTED';
properties: {
reason?: string;
};
} | {
event: 'NETWORK_CHANGED';
properties: {
chainId: number;
apiUrls: Record<string, SmartDappApiUrl>;
};
};With discriminated unions, TypeScript provides excellent IntelliSense and type safety. The clean structure makes it easy to handle events:
smartDapp.subscribeToChanges((event) => {
switch (event.event) {
case 'CONNECTED':
// TypeScript knows event.properties.address exists
console.log('Connected to:', event.properties.address);
break;
case 'DISCONNECTED':
// TypeScript knows event.properties.reason exists
console.log('Disconnected:', event.properties.reason);
break;
case 'NETWORK_CHANGED':
// TypeScript knows event.properties.chainId and event.properties.apiUrls exist
console.log('Network changed to:', event.properties.chainId);
console.log('API URLs:', event.properties.apiUrls);
break;
}
});SmartDapp follows a service-oriented architecture with clear separation of concerns:
SmartDapp
├── Web3InteropService # Wallet & network management
├── ContractService # Contract interactions
├── StorageService # Data persistence
└── LocalStorageAdapter # Storage implementation
- Web3InteropService: Handles wallet connections, network switching, and Web3 provider management
- ContractService: Manages contract interactions, ABI handling, and transaction processing
- StorageService: Provides network-aware storage with search capabilities
- LocalStorageAdapter: Implements storage interface (can be swapped for other storage backends)
// Subscribe to all state changes - this is the ONLY way to get state updates
smartDapp.subscribeToChanges((event: SmartDappEvent) => void)
// Available events with type-safe properties:
// - "CONNECTED": event.properties.address
// - "DISCONNECTED": event.properties.reason
// - "NETWORK_CHANGED": event.properties.chainId, event.properties.apiUrls// Connection actions
await smartDapp.openAppKitModal()
await smartDapp.closeAppKitModal()
// Network management
await smartDapp.selectNetwork(chainId)
// Configuration access (read-only, not reactive)
smartDapp.getCurrentCustomNetworkId()
smartDapp.getNetworks()// Read operations
await smartDapp.readCall(contractName, methodName, args?, staticCall?, value?, abiName?)
// Write operations
await smartDapp.sendTransaction(contractName, methodName, args?, value?, abiName?)
// Utility
await smartDapp.encodeFunctionData(contractName, methodName, args?, abiName?)// Storage
smartDapp.storeMetadata(key, value)
smartDapp.retrieveMetadata(key)
smartDapp.createCacheKey(key, ...params)
// Search
await smartDapp.findContentByKeyAndQuery(key, query)// Good: Subscribe to events and manage state reactively with type safety
let currentNetwork: number | null = null;
let currentAddress: string | null = null;
let currentApiUrls: Record<string, SmartDappApiUrl> = {};
smartDapp.subscribeToChanges((event) => {
switch (event.event) {
case "CONNECTED":
currentAddress = event.properties.address; // TypeScript knows this exists
updateUI();
break;
case "NETWORK_CHANGED":
currentNetwork = event.properties.chainId; // TypeScript knows this exists
currentApiUrls = event.properties.apiUrls; // TypeScript knows this exists
updateUI();
// Now you can use currentApiUrls.backend.url, currentApiUrls.markets.url, etc.
break;
case "DISCONNECTED":
currentAddress = null;
currentNetwork = null;
currentApiUrls = {};
updateUI();
break;
}
});
// Bad: Trying to use getters (they don't exist!)
// const network = smartDapp.getNetworkId(); // This method doesn't exist!
// const address = smartDapp.getAddress(); // This method doesn't exist!// Good: Centralized configuration
const config = {
networks: {
1: { /* mainnet config */ },
137: { /* polygon config */ }
},
apiUrls: {
1: { backend: { url: "https://api-mainnet.com" } },
137: { backend: { url: "https://api-polygon.com" } }
}
};
// Bad: Hardcoded values scattered throughout code
const mainnetApi = "https://api-mainnet.com";
const polygonApi = "https://api-polygon.com";// Good: Proper error handling
try {
const tx = await smartDapp.sendTransaction("Router", "swapExactETHForTokens", args, value);
console.log("Transaction successful:", tx.hash);
} catch (error) {
if (error.message.includes("insufficient funds")) {
// Handle specific error
} else {
// Handle general error
}
}
// Bad: No error handling
const tx = await smartDapp.sendTransaction("Router", "swapExactETHForTokens", args, value);// Good: Track network state and API URLs through events with type safety
let currentNetwork: number | null = null;
let currentApiUrls: Record<string, SmartDappApiUrl> = {};
smartDapp.subscribeToChanges((event) => {
if (event.event === "NETWORK_CHANGED") {
currentNetwork = event.properties.chainId; // TypeScript knows this exists
currentApiUrls = event.properties.apiUrls; // TypeScript knows this exists
handleNetworkChange(currentNetwork, currentApiUrls);
}
});
function handleNetworkChange(networkId: number, apiUrls: Record<string, SmartDappApiUrl>) {
if (networkId === 1) {
// Ethereum mainnet logic
console.log("Using mainnet API:", apiUrls.backend.url);
} else if (networkId === 137) {
// Polygon logic
console.log("Using Polygon API:", apiUrls.backend.url);
}
// Use the correct API URLs for the current network
fetch(`${apiUrls.backend.url}/user-data`)
.then(response => response.json())
.then(data => console.log(data));
}
// Bad: Assuming network or trying to use getters
const apiUrl = "https://api-mainnet.com"; // Always mainnet!
// const network = smartDapp.getNetworkId(); // This method doesn't exist!// Good: Use network-aware storage
smartDapp.storeMetadata("userPreferences", {
theme: "dark",
language: "en"
});
// Bad: Global storage without network context
localStorage.setItem("userPreferences", JSON.stringify(prefs));class CustomStorageAdapter implements StorageAdapter {
getItem<Storable extends Object>(key: string): Storable | null {
// Your custom storage logic
return JSON.parse(localStorage.getItem(key) || 'null');
}
setItem<Storable extends Object>(key: string, value: Storable): void {
localStorage.setItem(key, JSON.stringify(value));
}
removeItem(key: string): void {
localStorage.removeItem(key);
}
}
const smartDapp = new SmartDapp(config, false, new CustomStorageAdapter());// Enable testnet networks
const smartDapp = new SmartDapp(config, true); // true = developer mode// Use direct addresses with ABI names
const tx = await smartDapp.sendTransaction(
"0x123...", // Direct address
"transfer",
["0x456...", "1000000"],
undefined,
"ERC20" // ABI name
);We welcome contributions! Please see our Contributing Guide for details.
git clone https://github.com/saefstroem/SmartDapp.git
cd SmartDapp
npm install
npm run devISC License - see LICENSE file for details.
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Documentation: Full API Docs
SmartDapp - Event-driven Web3 development that prevents state management nightmares.