Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/flat-melons-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@xchainjs/xchain-evm': minor
---

Added caching for better performance
74 changes: 74 additions & 0 deletions packages/xchain-evm/__tests__/cache.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { JsonRpcProvider } from 'ethers'
import { getCachedContract } from '../src/cache'
import erc20ABI from '../src/data/erc20.json'

describe('Contract Cache', () => {
it('should cache contracts separately for different providers', () => {
// Create two different providers
const provider1 = new JsonRpcProvider('https://eth.llamarpc.com')
const provider2 = new JsonRpcProvider('https://goerli.infura.io/v3/test')

const contractAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // USDC address

// Get contracts from both providers
const contract1 = getCachedContract(contractAddress, erc20ABI, provider1)
const contract2 = getCachedContract(contractAddress, erc20ABI, provider2)

// Contracts should have different providers
expect(contract1.runner).toBe(provider1)
expect(contract2.runner).toBe(provider2)
expect(contract1).not.toBe(contract2) // Different contract instances

// Getting the same contract again should return the cached instance
const contract1Again = getCachedContract(contractAddress, erc20ABI, provider1)
expect(contract1).toBe(contract1Again) // Same instance from cache
})

it('should cache contracts separately for different addresses on same provider', () => {
const provider = new JsonRpcProvider('https://eth.llamarpc.com')

const address1 = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // USDC
const address2 = '0xdAC17F958D2ee523a2206206994597C13D831ec7' // USDT

// Get contracts for different addresses
const contract1 = getCachedContract(address1, erc20ABI, provider)
const contract2 = getCachedContract(address2, erc20ABI, provider)

// Should be different contract instances
expect(contract1).not.toBe(contract2)
expect(contract1.target).toBe(address1)
expect(contract2.target).toBe(address2)

// Getting the same contract again should return the cached instance
const contract1Again = getCachedContract(address1, erc20ABI, provider)
expect(contract1).toBe(contract1Again)
})

it('should cache contracts separately for different provider instances with same URL', () => {
// Create two distinct provider instances using the same URL
const sameUrl = 'https://eth.llamarpc.com'
const provider1 = new JsonRpcProvider(sameUrl)
const provider2 = new JsonRpcProvider(sameUrl)

const contractAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // USDC address

// Get contracts from both provider instances
const contract1 = getCachedContract(contractAddress, erc20ABI, provider1)
const contract2 = getCachedContract(contractAddress, erc20ABI, provider2)

// Contracts should be different instances (cache keys by provider instance, not just URL)
expect(contract1).not.toBe(contract2)

// Each contract should be bound to its respective provider
expect(contract1.runner).toBe(provider1)
expect(contract2.runner).toBe(provider2)
expect(contract1.runner).not.toBe(contract2.runner)

// Calling getCachedContract again with the same provider should return the cached instance
const contract1Again = getCachedContract(contractAddress, erc20ABI, provider1)
const contract2Again = getCachedContract(contractAddress, erc20ABI, provider2)

expect(contract1).toBe(contract1Again) // Same instance from cache for provider1
expect(contract2).toBe(contract2Again) // Same instance from cache for provider2
})
})
54 changes: 54 additions & 0 deletions packages/xchain-evm/src/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Contract } from 'ethers'
import type { Provider, InterfaceAbi } from 'ethers'
import BigNumber from 'bignumber.js'

// Provider-scoped contract cache using WeakMap for automatic cleanup
const contractCacheByProvider: WeakMap<Provider, Map<string, Contract>> = new WeakMap()
const bigNumberCache = new Map<string, BigNumber>()

/**
* Get a cached Contract instance or create a new one
* Uses provider-scoped caching for proper isolation
*/
export function getCachedContract(address: string, abi: InterfaceAbi, provider: Provider): Contract {
// Get or create the contract cache for this provider
let providerCache = contractCacheByProvider.get(provider)
if (!providerCache) {
providerCache = new Map<string, Contract>()
contractCacheByProvider.set(provider, providerCache)
}

// Use normalized address as key
const normalizedAddress = address.toLowerCase()

// Get or create the contract for this address
let contract = providerCache.get(normalizedAddress)
if (!contract) {
contract = new Contract(address, abi, provider)
providerCache.set(normalizedAddress, contract)
}

return contract
}

/**
* Get a cached BigNumber instance or create a new one
* Only accepts string or bigint to preserve precision
*/
export function getCachedBigNumber(value: string | bigint): BigNumber {
const stringValue = typeof value === 'bigint' ? value.toString() : value
if (!bigNumberCache.has(stringValue)) {
bigNumberCache.set(stringValue, new BigNumber(stringValue))
}
return bigNumberCache.get(stringValue)!
}
Comment on lines +38 to +44
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this cache trying to optimize? It seems that it only saves the BigNumber instance. Is this really a costly process?


/**
* Clear all caches (useful for testing or memory management)
* Note: WeakMap-based contract cache will be automatically cleaned up by GC
*/
export function clearCaches(): void {
// Note: WeakMap doesn't have a clear() method, and that's by design
// The contract cache will be automatically cleaned up when providers are GC'd
bigNumberCache.clear()
}
Loading