@gibs/common
v1.20.0
Published
Low-level blockchain helpers shared across the Gibs Finance packages: caching, multicall, and serialization.
Downloads
1,099
Maintainers
Readme
Common Utilities
A collection of essential blockchain utilities and helpers that provide foundational functionality for decentralized applications. This package contains reusable, blockchain-agnostic tools that are not specific to bridge operations but commonly needed across DeFi applications.
🎯 Use Cases
Primary Use Cases
- RPC Client Management: Create and cache blockchain RPC connections with fallback support
- Token Operations: Load ERC20 token metadata directly from blockchain contracts
- Data Serialization: Handle BigInt serialization in JSON for blockchain data
- Performance Optimization: Cache expensive blockchain calls and computations
- Batch Operations: Execute multiple blockchain calls efficiently using multicall
Target Applications
- Multi-chain DeFi applications
- Blockchain data aggregators
- Token analytics platforms
- Wallet backends
- DApp infrastructure
- Bridge applications (used by
@gibs/bridge-sdk)
🔧 Core Functionality
RPC Client Management
- Connection Pooling: Cached RPC clients to avoid duplicate connections
- Fallback Support: Automatic failover between multiple RPC endpoints
- WebSocket & HTTP: Support for both HTTP and WebSocket connections
- Batch Configuration: Optimized batching for better performance
ERC20 Token Operations
- Metadata Loading: Fetch token name, symbol, decimals from blockchain
- Multiple ABI Support: Handles both standard and bytes32 symbol formats
- Caching: Memoized token calls to reduce redundant blockchain requests
- Error Handling: Graceful fallbacks for non-standard token contracts
Advanced Caching
- TTL Caching: Time-based cache expiration for data freshness
- Size-Limited Caching: LRU-style caching with maximum entry limits
- Serialization Support: Cache complex objects including BigInt values
- Memory Efficient: Automatic cleanup of expired cache entries
Multicall Operations
- Batch Calls: Execute multiple contract calls in a single transaction
- Gas Optimization: Reduce gas costs through call aggregation
- Type Safety: Fully typed multicall results
- Error Resilience: Handle partial failures in batch operations
📦 Installation
npm install @gibs/common viem
# or
yarn add @gibs/common viemWays to use the package
The package ships two builds of the same code. The exports map picks one, so
you need no configuration.
| Option | What you write | What loads |
|---|---|---|
| ECMAScript modules | import { memoizeByKey } from '@gibs/common/cache' | dist/cache.js |
| CommonJS | const { memoizeByKey } = require('@gibs/common/cache') | dist/cjs/cache.js |
| React Native (Metro) | either of the above | import gets dist/, require gets dist/cjs/ |
| React Native (jest) | either of the above | dist/cjs/ |
React Native 0.77 or later needs no change to metro.config.js, no
resolveRequest hook, no jest transformIgnorePatterns and no polyfill.
It runs on React Native's own URL and URLSearchParams, which React Native
0.77 implements only in part: no code in the package reads them.
Metro's default configuration in React Native 0.79 or later reads the
exports map. Metro in React Native 0.77 and 0.78 ignores
exports: it reads the react-native field for the root, and a folder the
package ships for each subpath, such as cache/package.json, which points at
the CommonJS build.
📖 API Examples
Client (@gibs/common/client)
import {
clientFromChain,
chainKey,
clientCache,
defaultBatchConfig
} from '@gibs/common/client'
import { mainnet, polygon } from 'viem/chains'
// Create a cached RPC client with fallback URLs
const client = clientFromChain({
chain: mainnet,
urls: [
'https://mainnet.infura.io/v3/your-key',
'https://eth-mainnet.g.alchemy.com/v2/your-key'
]
})
// Create client with custom batch configuration
const polygonClient = clientFromChain({
chain: polygon,
urls: ['https://polygon-rpc.com']
})
// Generate cache key for debugging
const key = chainKey(1, ['https://rpc.url']) // "1,https://rpc.url"
const multiKey = chainKey(137, ['https://rpc1.url', 'https://rpc2.url']) // "137,https://rpc1.url,https://rpc2.url"
// Access client cache directly
const cached = clientCache.get(1) // { key: string, client: PublicClient } | undefined
console.log(cached?.key) // Cache key used for this client
// View default batch configuration
console.log(defaultBatchConfig)
// {
// batch: {
// wait: 10, // Wait 10ms before batching
// batchSize: 32, // Maximum 32 calls per batch
// }
// }
// Clear cache entry
clientCache.delete(1)
// Check cache size
console.log(clientCache.size) // Number of cached clientsERC20 (@gibs/common/erc20)
import { multicallErc20, erc20MetadataCalls } from '@gibs/common/erc20'
import { mainnet, arbitrum } from 'viem/chains'
// Load token metadata (with automatic caching)
const tokenAddress = '0xA0b86a33E6441e4f4a1b95c0B5a7F1ac03c7B3B0'
const [name, symbol, decimals] = await multicallErc20({
client,
target: tokenAddress,
chain: mainnet
})
console.log(name) // "Wrapped Ether"
console.log(symbol) // "WETH"
console.log(decimals) // 18
// Load multiple tokens efficiently
const tokens = [
'0xA0b86a33E6441e4f4a1b95c0B5a7F1ac03c7B3B0', // WETH
'0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
'0xdAC17F958D2ee523a2206206994597C13D831ec7' // USDT
]
const tokenMetadata = await Promise.all(
tokens.map(async (address) => {
const [name, symbol, decimals] = await multicallErc20({
client,
target: address,
chain: mainnet
})
return { name, symbol, decimals, address }
})
)
// Handle tokens with bytes32 symbols (automatically handled)
const [usdcName, usdcSymbol, usdcDecimals] = await multicallErc20({
client,
target: '0xA0b86a33E6441e4f4a1b95c0B5a7F1ac03c7B3B0',
chain: mainnet
}) // Falls back to bytes32 ABI if standard fails
// View the standard metadata calls used
console.log(erc20MetadataCalls)
// [
// { functionName: 'name' },
// { functionName: 'symbol' },
// { functionName: 'decimals' }
// ]Serialize (@gibs/common/serialize)
import {
jsonAnyStringify,
jsonAnyParse,
isSerializedBigInt
} from '@gibs/common/serialize'
// Serialize data with BigInt values
const data = {
balance: 1500000000000000000n, // 1.5 ETH
user: '0x123...',
timestamp: Date.now(),
amounts: [100n, 200n, 300n], // Array with BigInts
nested: {
value: 999999999999999999n
}
}
// Convert to JSON string
const jsonString = JSON.stringify(data, jsonAnyStringify)
console.log(jsonString)
// '{"balance":{"__type__":"bigint","value":"1500000000000000000"},"user":"0x123...","timestamp":1699123456789,"amounts":[{"__type__":"bigint","value":"100"},{"__type__":"bigint","value":"200"},{"__type__":"bigint","value":"300"}],"nested":{"value":{"__type__":"bigint","value":"999999999999999999"}}}'
// Parse back to original types
const parsed = JSON.parse(jsonString, jsonAnyParse)
console.log(typeof parsed.balance) // "bigint"
console.log(parsed.balance === 1500000000000000000n) // true
console.log(parsed.amounts[0] === 100n) // true
// Check if value is serialized BigInt
const serializedValue = { __type__: 'bigint', value: '123' }
console.log(isSerializedBigInt(serializedValue)) // true
console.log(isSerializedBigInt({ type: 'bigint', value: '123' })) // false
console.log(isSerializedBigInt('123')) // false
// Use in localStorage/sessionStorage
const cacheData = { price: 1234567890123456789n, lastUpdate: Date.now() }
localStorage.setItem('cache', JSON.stringify(cacheData, jsonAnyStringify))
const restored = JSON.parse(localStorage.getItem('cache')!, jsonAnyParse)
console.log(typeof restored.price) // "bigint"Cache (@gibs/common/cache)
import { maxMemoize, ttlMemoizeSingle } from '@gibs/common/cache'
// Size-limited caching (default: 1024 entries)
const expensiveCalc = maxMemoize((x: number, y: number) => {
console.log(`Computing ${x} * ${y}...`) // Only logs on cache miss
return x * y * Math.random()
}, 100) // Limit to 100 entries
console.log(expensiveCalc(5, 10)) // Computes and caches
console.log(expensiveCalc(5, 10)) // Returns cached result
console.log(expensiveCalc(3, 7)) // Computes new value
// TTL-based caching (default: 1 hour)
const fetchPrice = ttlMemoizeSingle((symbol: string) => {
console.log(`Fetching price for ${symbol}...`)
return fetch(`/api/price/${symbol}`).then(r => r.json())
}, 30000) // Cache for 30 seconds
const ethPrice1 = await fetchPrice('ETH') // Fetches from API
const ethPrice2 = await fetchPrice('ETH') // Returns cached result (if within 30s)
// Advanced caching with BigInt serialization
const tokenPriceCache = maxMemoize((tokenAddress: string, chainId: number) => {
console.log(`Loading price for ${tokenAddress} on chain ${chainId}`)
return {
price: 1500000000000000000n, // BigInt price
timestamp: Date.now()
}
}, 500)
const wethPrice = tokenPriceCache('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', 1)
console.log(typeof wethPrice.price) // "bigint"
// TTL cache with complex return types
const getBlockInfo = ttlMemoizeSingle(async (blockNumber: bigint) => {
return {
number: blockNumber,
timestamp: BigInt(Date.now()),
hash: '0x...',
gasUsed: 21000n
}
}, 15000) // Cache for 15 seconds
const block = await getBlockInfo(18500000n)
console.log(typeof block.number) // "bigint"Multicall (@gibs/common/multicall)
import { multicallRead } from '@gibs/common/multicall'
import { erc20Abi } from 'viem'
// Basic multicall - batch multiple contract calls efficiently
const calls = [
{ functionName: 'balanceOf', args: ['0x123...'] },
{ functionName: 'allowance', args: ['0x123...', '0x456...'] },
{ functionName: 'totalSupply', args: [] }
]
const results = await multicallRead({
client,
chain: mainnet,
abi: erc20Abi,
target: '0xA0b86a33E6441e4f4a1b95c0B5a7F1ac03c7B3B0', // WETH
calls
})
const [balance, allowance, totalSupply] = results
console.log('Balance:', balance) // bigint
console.log('Allowance:', allowance) // bigint
console.log('Total Supply:', totalSupply) // bigint
// Advanced multicall with different targets and ABIs
const mixedCalls = [
{
functionName: 'balanceOf',
args: ['0x123...'],
target: '0xA0b86a33E6441e4f4a1b95c0B5a7F1ac03c7B3B0', // WETH
abi: erc20Abi
},
{
functionName: 'balanceOf',
args: ['0x123...'],
target: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // DAI
abi: erc20Abi
},
{
functionName: 'name',
args: [],
target: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
abi: erc20Abi
}
]
const mixedResults = await multicallRead({
client,
chain: mainnet,
abi: erc20Abi, // Default ABI
calls: mixedCalls
})
const [wethBalance, daiBalance, usdtName] = mixedResults
// Multicall with failure handling
const riskyCalls = [
{
functionName: 'balanceOf',
args: ['0x123...'],
target: '0xValidToken...',
allowFailure: false // Will throw if this fails
},
{
functionName: 'balanceOf',
args: ['0x123...'],
target: '0xMaybeInvalidToken...',
allowFailure: true // Will return error object instead of throwing
}
]
const riskyResults = await multicallRead({
client,
chain: mainnet,
abi: erc20Abi,
calls: riskyCalls
})
// riskyResults[1] might be an error object if the call failedTypes (@gibs/common/types)
import type { Erc20Metadata, Call } from '@gibs/common/types'
// ERC20 token metadata structure (returned by multicallErc20)
const tokenMetadata: Erc20Metadata = ['Wrapped Ether', 'WETH', 18]
const [name, symbol, decimals] = tokenMetadata
console.log(`${name} (${symbol}) has ${decimals} decimals`)
// Multicall call configuration
const balanceCall: Call = {
functionName: 'balanceOf',
args: ['0x123...'],
target: '0xTokenAddress...',
abi: erc20Abi,
allowFailure: false
}
const nameCall: Call = {
functionName: 'name',
args: [],
// target and abi will be inherited from multicallRead params
allowFailure: true
}
// Use in function signatures
function processTokenMetadata(metadata: Erc20Metadata) {
const [name, symbol, decimals] = metadata
console.log(`Processing ${symbol} (${name}) with ${decimals} decimals`)
return {
displayName: `${name} (${symbol})`,
precision: decimals,
isStablecoin: name.toLowerCase().includes('usd')
}
}
function buildMulticall(calls: Call[]) {
return multicallRead({
client,
chain: mainnet,
abi: erc20Abi,
calls
})
}
// Advanced Call configurations
const complexCalls: Call[] = [
{
functionName: 'getReserves',
args: [],
target: '0xPairAddress...',
abi: pairAbi,
allowFailure: false
},
{
functionName: 'token0',
args: [],
target: '0xPairAddress...',
abi: pairAbi,
allowFailure: false
},
{
functionName: 'token1',
args: [],
target: '0xPairAddress...',
abi: pairAbi,
allowFailure: false
}
]🔧 Configuration
Default Batch Configuration
import { defaultBatchConfig } from '@gibs/common/client'
console.log(defaultBatchConfig)
// {
// batch: {
// wait: 10, // Wait 10ms before batching
// batchSize: 32, // Maximum 32 calls per batch
// }
// }
// This configuration is automatically applied to all HTTP transports
// WebSocket transports get additional configuration:
// {
// keepAlive: true,
// reconnect: true,
// retryDelay: 250,
// retryCount: 10,
// timeout: 4000,
// ...defaultBatchConfig
// }📋 API Reference
Exports
| Export Path | Description |
|-------------|-------------|
| @gibs/common | Main entry point with all exports |
| @gibs/common/client | RPC client creation and management |
| @gibs/common/erc20 | ERC20 token metadata utilities |
| @gibs/common/cache | Caching and memoization functions |
| @gibs/common/serialize | JSON serialization with BigInt support |
| @gibs/common/multicall | Batch contract call utilities |
| @gibs/common/types | TypeScript type definitions |
| @gibs/common/url | Request addresses built and read without URL or URLSearchParams |
Key Functions
Client Management
// Create cached RPC client with fallback support
function clientFromChain(params: {
chain: Chain
urls: string[]
}): PublicClient
// Generate cache key for client identification
function chainKey(chainId: number, urls: string[]): string
// Client cache Map
const clientCache: Map<number, { key: string, client: PublicClient }>ERC20 Operations
// Load ERC20 metadata with caching and fallback ABIs
function multicallErc20(params: {
client: PublicClient
target: Hex
chain: Chain
}): Promise<[string, string, number]> // [name, symbol, decimals]
// Standard ERC20 metadata calls
const erc20MetadataCalls: readonly [
{ functionName: 'name' },
{ functionName: 'symbol' },
{ functionName: 'decimals' }
]Caching
// Size-limited memoization
function maxMemoize<A extends unknown[], B>(
fn: (...a: A) => B,
max?: number
): (...a: A) => B
// TTL-based single value caching
function ttlMemoizeSingle<A extends unknown[], B>(
fn: (...a: A) => B,
ttl?: number
): (...a: A) => B
// One answer per resolver key, never expired; `cache` is a Map you can evict from
function memoizeByKey<A extends unknown[], B, K>(
fn: (...args: A) => B,
resolver: (...args: A) => K
): ((...args: A) => B) & { cache: Map<K, B> }Serialization
// JSON replacer for BigInt serialization
function jsonAnyStringify(key: string, value: any): any
// JSON reviver for BigInt deserialization
function jsonAnyParse(key: string, value: any): any
// Type guard for serialized BigInt values
function isSerializedBigInt(value: unknown): value is SerializedBigIntMulticall
// Execute multiple contract calls in a single transaction
function multicallRead<T = readonly unknown[]>(params: {
client: PublicClient
chain: Chain
abi: Abi
target: Hex
calls: readonly unknown[]
}): Promise<T>Key Types
// ERC20 token metadata tuple
type Erc20Metadata = [string, string, number] // [name, symbol, decimals]
// Multicall call configuration
interface Call {
functionName: string
args?: (bigint | Hex | Hex[] | bigint[] | string | string[])[]
target?: Hex // Override default target
abi?: Abi // Override default ABI
allowFailure?: boolean // Default: false
}
// Serialized BigInt structure
interface SerializedBigInt {
__type__: 'bigint'
value: string
}⚡ Performance Features
Client Caching
- RPC clients are cached by chain ID and URL combination
- Prevents unnecessary connection overhead
- Automatic cache invalidation when URLs change
Request Optimization
- Batch configuration reduces RPC call frequency
- Fallback RPC URLs provide reliability
- WebSocket connections for real-time data when available
Memory Management
- Size-limited caches prevent memory leaks
- TTL caches automatically expire stale data
- Efficient cache key generation for complex objects
🔗 Dependencies
viem(peer dependency): Type-safe Ethereum library for blockchain interactions
There are no other runtime dependencies.
📝 Best Practices
Client Management
// ✅ Good: Reuse clients with consistent URLs
const client = clientFromChain({ chain: mainnet, urls: rpcUrls })
// ❌ Avoid: Creating new clients with different URL arrays each time
const client1 = clientFromChain({ chain: mainnet, urls: [url1, url2] })
const client2 = clientFromChain({ chain: mainnet, urls: [url2, url1] }) // Different cache key!Caching Strategy
// ✅ Good: Use appropriate cache types for your use case
const tokenMetadata = maxMemoize(fetchTokenMetadata, 500) // Bounded cache
const priceData = ttlMemoizeSingle(fetchPrices, 30000) // 30-second TTL
// ✅ Good: Cache expensive computations, not simple operations
const expensiveCalc = maxMemoize(heavyComputation)
const simpleCalc = (a, b) => a + b // Don't cache simple operationsError Handling
// ✅ Good: Handle multicall failures gracefully
try {
const results = await multicallRead({ ... })
} catch (error) {
console.warn('Multicall failed, falling back to individual calls')
// Implement fallback logic
}This package provides the foundation for robust, efficient blockchain applications by handling common infrastructure concerns so you can focus on your application logic.
