Skip to content

Commit d28c08d

Browse files
committed
update from CR comments
1 parent 853ff60 commit d28c08d

File tree

4 files changed

+81
-11
lines changed

4 files changed

+81
-11
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { JsonRpcProvider } from 'ethers'
2+
import { getCachedContract } from '../src/cache'
3+
import erc20ABI from '../src/data/erc20.json'
4+
5+
describe('Contract Cache', () => {
6+
it('should cache contracts separately for different providers', async () => {
7+
// Create two different providers
8+
const provider1 = new JsonRpcProvider('https://eth.llamarpc.com')
9+
const provider2 = new JsonRpcProvider('https://goerli.infura.io/v3/test')
10+
11+
const contractAddress = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // USDC address
12+
13+
// Get contracts from both providers
14+
const contract1 = await getCachedContract(contractAddress, erc20ABI, provider1)
15+
const contract2 = await getCachedContract(contractAddress, erc20ABI, provider2)
16+
17+
// Contracts should have different providers
18+
expect(contract1.runner).toBe(provider1)
19+
expect(contract2.runner).toBe(provider2)
20+
expect(contract1).not.toBe(contract2) // Different contract instances
21+
22+
// Getting the same contract again should return the cached instance
23+
const contract1Again = await getCachedContract(contractAddress, erc20ABI, provider1)
24+
expect(contract1).toBe(contract1Again) // Same instance from cache
25+
})
26+
27+
it('should cache contracts separately for different addresses on same provider', async () => {
28+
const provider = new JsonRpcProvider('https://eth.llamarpc.com')
29+
30+
const address1 = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' // USDC
31+
const address2 = '0xdAC17F958D2ee523a2206206994597C13D831ec7' // USDT
32+
33+
// Get contracts for different addresses
34+
const contract1 = await getCachedContract(address1, erc20ABI, provider)
35+
const contract2 = await getCachedContract(address2, erc20ABI, provider)
36+
37+
// Should be different contract instances
38+
expect(contract1).not.toBe(contract2)
39+
expect(contract1.target).toBe(address1)
40+
expect(contract2.target).toBe(address2)
41+
42+
// Getting the same contract again should return the cached instance
43+
const contract1Again = await getCachedContract(address1, erc20ABI, provider)
44+
expect(contract1).toBe(contract1Again)
45+
})
46+
})

packages/xchain-evm/src/cache.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,41 @@
11
import { Contract, Provider } from 'ethers'
22
import BigNumber from 'bignumber.js'
33

4-
// Global caches for Contract and BigNumber instances
4+
// Per-provider contract cache to ensure contracts are properly isolated
5+
// Key format: `${providerNetwork}_${chainId}_${address}`
56
const contractCache = new Map<string, Contract>()
67
const bigNumberCache = new Map<string, BigNumber>()
78

9+
/**
10+
* Generate a unique cache key for a contract that includes provider context
11+
*/
12+
async function getContractCacheKey(address: string, provider: Provider): Promise<string> {
13+
try {
14+
// Get network information from provider to create unique key
15+
const network = await provider.getNetwork()
16+
const chainId = network.chainId.toString()
17+
const networkName = network.name || 'unknown'
18+
return `${networkName}_${chainId}_${address.toLowerCase()}`
19+
} catch {
20+
// Fallback to a provider-specific key if network info unavailable
21+
// Use provider instance as unique identifier
22+
const providerIdentity = provider as any
23+
const connectionUrl = providerIdentity._request?.url ||
24+
providerIdentity.connection?.url ||
25+
providerIdentity._url ||
26+
'unknown'
27+
const hashedKey = Buffer.from(connectionUrl.toString()).toString('base64').replace(/[^a-zA-Z0-9]/g, '').slice(0, 10)
28+
return `provider_${hashedKey}_${address.toLowerCase()}`
29+
}
30+
}
31+
832
/**
933
* Get a cached Contract instance or create a new one
34+
* Now includes provider/network isolation to prevent cross-network contract reuse
1035
*/
1136
// eslint-disable-next-line @typescript-eslint/no-explicit-any
12-
export function getCachedContract(address: string, abi: any, provider: Provider): Contract {
13-
// Use address as key (contracts are address-specific anyway)
14-
const key = address.toLowerCase()
37+
export async function getCachedContract(address: string, abi: any, provider: Provider): Promise<Contract> {
38+
const key = await getContractCacheKey(address, provider)
1539
if (!contractCache.has(key)) {
1640
contractCache.set(key, new Contract(address, abi, provider))
1741
}

packages/xchain-evm/src/clients/client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -453,7 +453,7 @@ export class Client extends BaseXChainClient implements EVMClient {
453453
// ERC20 gas estimate
454454
const assetAddress = getTokenAddress(theAsset)
455455
if (!assetAddress) throw Error(`Can't get address from asset ${assetToString(theAsset)}`)
456-
const contract = getCachedContract(assetAddress, erc20ABI, this.getProvider())
456+
const contract = await getCachedContract(assetAddress, erc20ABI, this.getProvider())
457457

458458
const address = from || (await this.getAddressAsync())
459459
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -703,7 +703,7 @@ export class Client extends BaseXChainClient implements EVMClient {
703703
const assetAddress = getTokenAddress(asset)
704704
if (!assetAddress) throw Error(`Can't parse address from asset ${assetToString(asset)}`)
705705

706-
const contract = getCachedContract(assetAddress, erc20ABI, this.getProvider())
706+
const contract = await getCachedContract(assetAddress, erc20ABI, this.getProvider())
707707

708708
const amountToTransfer = BigInt(amount.amount().toFixed())
709709
const unsignedTx = await contract.getFunction('transfer').populateTransaction(recipient, amountToTransfer)
@@ -736,7 +736,7 @@ export class Client extends BaseXChainClient implements EVMClient {
736736
if (!this.validateAddress(spenderAddress)) throw Error('Invalid spenderAddress address')
737737
if (!this.validateAddress(sender)) throw Error('Invalid sender address')
738738

739-
const contract = getCachedContract(contractAddress, erc20ABI, this.getProvider())
739+
const contract = await getCachedContract(contractAddress, erc20ABI, this.getProvider())
740740
const valueToApprove = getApprovalAmount(amount)
741741

742742
const unsignedTx = await contract

packages/xchain-evm/src/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export const estimateCall = async ({
120120
funcName: string
121121
funcParams?: unknown[]
122122
}): Promise<BigNumber> => {
123-
const contract = getCachedContract(contractAddress, abi, provider)
123+
const contract = await getCachedContract(contractAddress, abi, provider)
124124
const estiamtion = await contract.getFunction(funcName).estimateGas(...funcParams)
125125
return getCachedBigNumber(estiamtion.toString())
126126
}
@@ -150,7 +150,7 @@ export const call = async <T>({
150150
funcName: string
151151
funcParams?: unknown[]
152152
}): Promise<T> => {
153-
let contract: BaseContract = getCachedContract(contractAddress, abi, provider)
153+
let contract: BaseContract = await getCachedContract(contractAddress, abi, provider)
154154
if (signer) {
155155
// For sending transactions, a signer is needed
156156
contract = contract.connect(signer)
@@ -176,7 +176,7 @@ export const getContract = async ({
176176
contractAddress: Address
177177
abi: InterfaceAbi
178178
}): Promise<Contract> => {
179-
return getCachedContract(contractAddress, abi, provider)
179+
return await getCachedContract(contractAddress, abi, provider)
180180
}
181181

182182
/**
@@ -240,7 +240,7 @@ export async function isApproved({
240240
amount?: BaseAmount
241241
}): Promise<boolean> {
242242
const txAmount = getCachedBigNumber(amount?.amount().toFixed() ?? '1')
243-
const contract = getCachedContract(contractAddress, erc20ABI, provider)
243+
const contract = await getCachedContract(contractAddress, erc20ABI, provider)
244244
const allowanceResponse = await contract.allowance(fromAddress, spenderAddress)
245245
const allowance = getCachedBigNumber(allowanceResponse.toString())
246246

0 commit comments

Comments
 (0)