Network-Aware Database Utils
The prismaNetworkUtils
module provides an elegant abstraction layer that automatically routes database operations to the correct network-specific table. This critical infrastructure component enables RitoSwap to support multiple blockchain networks while maintaining clean, network-agnostic code throughout the application.
Core Concept
In a multichain dApp, the same token ID can exist across different networks, representing entirely different assets. Token #42 on Ethereum mainnet is not the same as token #42 on Sepolia testnet. The prismaNetworkUtils module solves this challenge by providing a unified interface that automatically selects the appropriate database table based on the current network context.
The Problem It Solves
Without this abstraction, every database query would need to manually determine which table to use:
// ❌ Without prismaNetworkUtils - Error prone and repetitive
let token;
const chainId = getTargetChainId();
if (chainId === 1) {
token = await prisma.tokenEthereum.findUnique({ where: { tokenId: 42 } });
} else if (chainId === 11155111) {
token = await prisma.tokenSepolia.findUnique({ where: { tokenId: 42 } });
} else if (chainId === 90999999) {
token = await prisma.tokenRitonet.findUnique({ where: { tokenId: 42 } });
}
// ✅ With prismaNetworkUtils - Clean and maintainable
const tokenModel = getTokenModel();
const token = await tokenModel.findUnique({ where: { tokenId: 42 } });
Module Exports
The prismaNetworkUtils module exports two primary functions:
getTokenModel()
Returns a unified interface for token operations that internally routes to the correct Prisma model based on the current chain ID.
interface TokenOperations {
findUnique: (args: { where: { tokenId: number } }) => Promise<any>
findMany: (args?: any) => Promise<any[]>
upsert: (args: {
where: { tokenId: number }
update: any
create: any
}) => Promise<any>
}
Usage Example:
import { getTokenModel } from '@/app/lib/prisma/prismaNetworkUtils'
// In an API route or server component
const tokenModel = getTokenModel();
// Find a specific token
const token = await tokenModel.findUnique({
where: { tokenId: 123 }
});
// Find all used tokens
const usedTokens = await tokenModel.findMany({
where: { used: true },
orderBy: { usedAt: 'desc' }
});
// Create or update a token record
const updatedToken = await tokenModel.upsert({
where: { tokenId: 456 },
update: { used: true, usedBy: address, usedAt: new Date() },
create: { tokenId: 456, used: true, usedBy: address, usedAt: new Date() }
});
getChainConfig()
Provides comprehensive configuration for the current blockchain network, including RPC endpoints and chain metadata.
interface ChainConfig {
chain: {
id: number
name: string
network: string
nativeCurrency: {
decimals: number
name: string
symbol: string
}
rpcUrls: {
default: { http: string[] }
public: { http: string[] }
}
}
transport: string
}
Usage Example:
import { getChainConfig } from '@/app/lib/prisma/prismaNetworkUtils'
import { createPublicClient, http } from 'viem'
// Get configuration for the current network
const chainConfig = getChainConfig();
// Create a blockchain client with the correct configuration
const publicClient = createPublicClient({
chain: chainConfig.chain,
transport: http(chainConfig.transport)
});
// Use the client for on-chain operations
const balance = await publicClient.getBalance({
address: '0x...'
});
Implementation Details
Network Detection
The module relies on the getTargetChainId()
function from chainConfig.ts
to determine the current network. This function checks environment variables in a specific order of precedence:
- RitoNet (Local) -
NEXT_PUBLIC_RITONET === 'true'
- Sepolia (Testnet) -
NEXT_PUBLIC_SEPOLIA === 'true'
- Ethereum (Mainnet) - Default if no flags are set
Table Mapping
Each supported network maps to a specific Prisma model:
Network | Chain ID | Prisma Model | Database Table |
---|---|---|---|
RitoNet | 2025 | prisma.tokenRitonet | token_ritonet |
Sepolia | 11155111 | prisma.tokenSepolia | token_sepolia |
Ethereum | 1 | prisma.tokenEthereum | token_ethereum |
Error Handling
The module implements defensive programming practices to handle edge cases:
export function getTokenModel(): TokenOperations {
const chainId = getTargetChainId()
switch (chainId) {
case CHAIN_IDS.RITONET:
// ... return RitoNet operations
case CHAIN_IDS.SEPOLIA:
// ... return Sepolia operations
case CHAIN_IDS.ETHEREUM:
// ... return Ethereum operations
default:
throw new Error(`Unsupported chain ID: ${chainId}`)
}
}
If an unsupported chain ID is detected, the function throws a descriptive error rather than silently failing or returning undefined behavior.
RPC Configuration
The getChainConfig()
function assembles RPC configurations dynamically based on environment variables:
Environment Variables Required:
NEXT_PUBLIC_LOCAL_BLOCKCHAIN_RPC
- RPC endpoint for local RitoNetNEXT_PUBLIC_ALCHEMY_API_KEY
- API key for Alchemy (used for Sepolia and Ethereum)NEXT_PUBLIC_LOCAL_CHAIN_ID
- Chain ID for RitoNet (defaults to 90999999)NEXT_PUBLIC_LOCAL_BLOCKCHAIN_NAME
- Display name for RitoNet (defaults to “RitoNet”)
Common Usage Patterns
Pattern 1: Token Status Checking
The most common use case involves checking whether a token exists and has been used:
// In /api/token-status/[tokenId]/route.ts
export async function GET(request: NextRequest, { params }: { params: { tokenId: string } }) {
const tokenId = parseInt(params.tokenId, 10);
const tokenModel = getTokenModel();
// Check database first
const token = await tokenModel.findUnique({
where: { tokenId }
});
if (token) {
return NextResponse.json({
exists: true,
used: token.used,
usedBy: token.usedBy,
usedAt: token.usedAt
});
}
// Token not in database, check blockchain...
}
Pattern 2: Marking Tokens as Used
When processing token-gated access, the pattern involves verification followed by status update:
// In /api/gate-access/route.ts
export async function POST(request: NextRequest) {
const { tokenId, address } = await request.json();
// Verify ownership on-chain first
const chainConfig = getChainConfig();
const publicClient = createPublicClient({
chain: chainConfig.chain,
transport: http(chainConfig.transport)
});
// ... ownership verification logic ...
// Update database
const tokenModel = getTokenModel();
await tokenModel.upsert({
where: { tokenId },
update: {
used: true,
usedBy: address,
usedAt: new Date()
},
create: {
tokenId,
used: true,
usedBy: address,
usedAt: new Date()
}
});
}
Pattern 3: Bulk Operations
For administrative or analytical purposes, bulk operations are straightforward:
// Find all unused tokens
const tokenModel = getTokenModel();
const unusedTokens = await tokenModel.findMany({
where: { used: false },
orderBy: { tokenId: 'asc' }
});
// Get usage statistics
const usageStats = await tokenModel.findMany({
where: { used: true },
select: {
usedAt: true,
usedBy: true
}
});
Testing Strategies
The module includes comprehensive test coverage demonstrating best practices for testing network-aware code:
Mock Setup
Tests use Vitest’s mocking capabilities to isolate the module from external dependencies:
vi.mock('@/app/utils/chainConfig', () => ({
getTargetChainId: vi.fn(),
CHAIN_IDS: { RITONET: 1, SEPOLIA: 2, ETHEREUM: 3 }
}))
vi.mock('@/app/lib/prisma/prisma', () => ({
prisma: {
tokenRitonet: { findUnique: vi.fn(), findMany: vi.fn(), upsert: vi.fn() },
tokenSepolia: { findUnique: vi.fn(), findMany: vi.fn(), upsert: vi.fn() },
tokenEthereum: { findUnique: vi.fn(), findMany: vi.fn(), upsert: vi.fn() },
}
}))
Test Scenarios
Key test cases verify:
- Correct model selection for each supported chain ID
- Proper parameter passing to underlying Prisma methods
- Error handling for unsupported chain IDs
- Configuration assembly with environment variables
Performance Considerations
The prismaNetworkUtils module is designed for optimal performance:
Minimal Overhead
The routing logic uses a simple switch statement with constant-time lookup, adding negligible overhead to database operations.
Connection Reuse
By routing through the singleton Prisma client, all operations benefit from connection pooling and query optimization.
Edge Caching
When used with Prisma Accelerate, frequently accessed tokens benefit from global edge caching, reducing database load and improving response times.
Migration and Maintenance
When adding support for new networks:
Step 1: Update Chain Configuration
Add the new chain ID to CHAIN_IDS
in chainConfig.ts
.
Step 2: Create Database Table
Add a new model to schema.prisma
following the existing pattern:
model TokenNewNetwork {
tokenId Int @id
used Boolean @default(false)
usedBy String?
usedAt DateTime?
@@map("token_newnetwork")
}
Step 3: Update Network Utils
Add a new case to both getTokenModel()
and getChainConfig()
functions.
Step 4: Run Migrations
Generate and apply the database migration to create the new table.
Step 5: Update Tests
Add test coverage for the new network in prismaNetworkUtils.test.ts
.
Troubleshooting
Common issues and their solutions:
“Unsupported chain ID” Error
Cause: The application is configured for a network not yet supported by prismaNetworkUtils.
Solution: Verify environment variables are set correctly. If adding a new network, follow the migration steps above.
Database Connection Errors
Cause: The Prisma client cannot connect to the database.
Solution: Check that DATABASE_URL
is set correctly and the database is accessible. For Prisma Accelerate, verify the connection string includes the API key.
Type Errors in TypeScript
Cause: The TypeScript types don’t match the actual Prisma schema.
Solution: Run pnpm prisma generate
to regenerate Prisma client types after schema changes.
Summary
The prismaNetworkUtils module exemplifies thoughtful abstraction design in a multichain environment. By providing a unified interface that transparently handles network-specific routing, it enables developers to write cleaner, more maintainable code while ensuring data isolation between networks. Combined with comprehensive testing and clear error handling, it forms a robust foundation for RitoSwap’s multichain token tracking infrastructure.