-
I want to reduce number of calls to RPC endpoint by using local caching database to store staticCalls with minimum diff in my code base. Ideally so whenever I call So far I tried few approaches including extending Anyone has ideas how to do this in a smart way? Below are code snippets I tried: 1. Trying to wrap in a Proxy: Getting error:
Code: class WithCache {
constructor(
original,
{
network,
actionProps,
staleTime = 3888000000 // 45 days
}
) {
this.kvsKey = [original.address, network].join(' ');
this.state = undefined;
this.store = new KeyValueStoreClient(actionProps);
this.staleTime = staleTime;
this.network = network;
// eslint-disable-next-line
return new Proxy(original, {
get: (target, prop, receiver) => {
const origMethod = Reflect.get(target, prop, receiver);
if (target?.interface?.functions) {
console.log('prop', prop);
console.log('target?.interface?.functions[prop]', target?.interface?.functions[prop]);
console.log(
'2nd',
Object.values(target.interface.functions).find((v) => v.name === prop)
);
const { stateMutability } =
target?.interface?.functions[prop] ||
Object.values(target.interface.functions).find((v) => v.name === prop);
console.debug('stateMutability:', stateMutability);
if (typeof origMethod === 'function' && (stateMutability === 'view' || stateMutability === 'pure')) {
return async (...args) => {
if (!this.network) {
console.debug('Network was not set, setting up a key from chain Id');
this.network = target.provider.getNetwork().then((n) => n.chainId);
this.kvsKey = [original.address, this.network].join(' ');
}
if (!this.state) {
console.debug(`Local cache does not exist, pulling...`);
this.state = await this.store
.get(this.kvsKey)
.then((r) => (r && Object.keys(r).length > 0 ? JSON.parse(r) : {}));
}
console.debug(`local cache: ${this.state}`);
console.debug(
`Trying to get cache from store ${this.kvsKey} with key ${[prop, JSON.stringify(args)].join(' ')}`
);
const key = ethers.utils.hashMessage([prop, JSON.stringify(args)].join(' '));
const now = new Date().getTime();
const state = this.state[key];
if (state && state.updatedAt && now - new Date(state.updatedAt).getTime() < this.staleTime) {
console.debug(`Cached state found with last update timestamp ${state.updatedAt}, now is: ${now}`);
const { data } = state;
return Object.freeze(data);
}
return origMethod.apply(this, args).then(async (r) => {
console.debug(
`Cached state was ${
state
? `outdated: last update timestamp ${state.updatedAt}, now is: ${now}, staleTime: ${this.staleTime}`
: `not found`
}`
);
this.state[key] = { data: r, updatedAt: now };
await this.store.put(this.kvsKey, JSON.stringify(this.state));
console.debug(`updated cache for: ${[this.kvsKey, prop, JSON.stringify(args)].join(' ')}`);
return r;
});
};
}
return origMethod;
}
return origMethod;
}
});
}
} 2. Trying to extend in to CachedContract
Code function defineFakeReadOnly(object, name, value) {
Object.defineProperty(object, name, {
enumerable: true,
value,
writable: true
});
}
class WithCache extends Contract {
constructor(addressOrName, contractInterface, signerOrProvider, rest) {
super(addressOrName, contractInterface, signerOrProvider);
Object.values(this.interface.functions).forEach((fragment) => {
const sig = new ethers.utils.Interface([fragment]).getSighash(fragment);
// delete super[fragment.name];
defineFakeReadOnly(this, fragment.name, (args) => {
console.log('lol');
return super[sig](args);
});
});
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment
-
I ended up wrapping a provider instead. For anyone who might need same: const { DefenderRelayProvider } = require('@openzeppelin/defender-sdk-relay-signer-client/lib/ethers');
const { debug } = require('./debug');
/**
* CachedDefenderProvider extends DefenderRelayProvider and provides caching functionality for contract calls.
* It utilizes a key-value store client to store and retrieve cached contract data.
* @class
* @extends DefenderRelayProvider
*/
class CachedDefenderProvider extends DefenderRelayProvider {
#cacheNetwork;
constructor(props, kvsClient) {
super(props);
this.#cacheNetwork = props.network;
this.staleTime = props?.staleTime ?? 388800000; // 45 days
this.pruneNonLatest = props.pruneNonLatest ?? true;
this.state = undefined;
this.store = kvsClient;
}
#cleanNonLatest = (blockTag) => {
Object.keys(this.state).forEach((key) => {
if (key !== 'latest' && key !== blockTag) {
console.debug('invalidating cache for block', key);
delete this.state[key];
}
});
};
/**
* Executes a contract call and caches the result.
* @param {...Object} tx - The transaction objects.
* @returns {Promise<any>} - A promise that resolves to the result of the contract call.
*/
call = async (...tx) => {
const blockTag = tx[1] ?? 'latest';
const contractKey = [this.#cacheNetwork, tx[0].to].join(' ');
if (!this.state) {
debug(`Local cache does not exist, pulling...`);
this.state = await this.store.get(contractKey).then((r) => (r && Object.keys(r).length > 0 ? JSON.parse(r) : {}));
if (this.pruneNonLatest) this.#cleanNonLatest(blockTag);
}
const now = new Date().getTime();
const blockState = this.state[blockTag];
const state = blockState ? blockState[tx[0].data] ?? {} : {};
if (state && state?.updatedAt && now - new Date(state.updatedAt).getTime() < this.staleTime) {
debug(`Cached state found with last update timestamp ${state.updatedAt}, now is: ${now}`);
return state.data;
}
debug(
`Cached state was ${
state?.updatedAt
? `outdated: last update timestamp ${state.updatedAt}, now is: ${now}, staleTime: ${this.staleTime}`
: `not found`
}`
);
const result = await super.call(...tx);
if (!this.state[blockTag]) this.state[blockTag] = {};
this.state[blockTag][tx[0].data] = { data: result, updatedAt: now };
await this.store.put(contractKey, JSON.stringify(this.state));
debug(`updated cache for: ${[contractKey, tx[0].data, blockTag].join(' ')}`);
return result;
};
}
module.exports = { CachedDefenderProvider }; |
Beta Was this translation helpful? Give feedback.
I ended up wrapping a provider instead.
For anyone who might need same: