Skip to content

Capture RPC calls metrics #691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
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
1 change: 1 addition & 0 deletions jest.e2e.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ module.exports = {
},
setupFiles: ['<rootDir>/scripts/loadProductionEnvVars.ts'],
setupFilesAfterEnv: ['<rootDir>/jest.e2e.setup.js'],
globalTeardown: '<rootDir>/scripts/teardown.js'
}
9 changes: 9 additions & 0 deletions scripts/teardown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const fs = require('fs')

const teardown = () => {
console.log('Tests finished! Running global cleanup...')
const data = fs.readFileSync('.metrics.json', 'utf8')
console.log(JSON.parse(data))
}

module.exports = teardown
2 changes: 1 addition & 1 deletion src/apps/aave/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const hook: PositionsHook = {
return []
}

const client = getClient(networkId)
const client = getClient(networkId, 'aave')

const [reservesData] = await client.readContract({
address: aaveAddresses.uiPoolDataProvider,
Expand Down
6 changes: 3 additions & 3 deletions src/apps/aave/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const hook: ShortcutsHook = {
// amount in smallest unit
const amountToSupply = parseUnits(tokens[0].amount, tokenDecimals)

const client = getClient(networkId)
const client = getClient(networkId, 'aave')

const approvedAllowanceForSpender = await client.readContract({
address: tokenAddress,
Expand Down Expand Up @@ -154,7 +154,7 @@ const hook: ShortcutsHook = {
? maxUint256
: parseUnits(tokens[0].amount, tokenDecimals)

const client = getClient(networkId)
const client = getClient(networkId, 'aave')

const underlyingAssetAddress = await client.readContract({
address: tokenAddress,
Expand Down Expand Up @@ -190,7 +190,7 @@ const hook: ShortcutsHook = {
async onTrigger({ networkId, address }) {
const walletAddress = address as Address

const client = getClient(networkId)
const client = getClient(networkId, 'aave')

// Get a/v/sToken for which we can claim rewards
const reserveIncentiveData = await client.readContract({
Expand Down
2 changes: 1 addition & 1 deletion src/apps/allbridge/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const hook: PositionsHook = {
return []
}

const client = getClient(networkId)
const client = getClient(networkId, 'allbridge')

const [balances, rewards, totalSupplies, lpTokenDecimals] =
await Promise.all([
Expand Down
2 changes: 1 addition & 1 deletion src/apps/allbridge/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const hook: ShortcutsHook = {
// amount in smallest unit
const amountToSupply = parseUnits(tokens[0].amount, tokenDecimals)

const client = getClient(networkId)
const client = getClient(networkId, 'allbridge')

const approvedAllowanceForSpender = await client.readContract({
address: tokenAddress,
Expand Down
4 changes: 2 additions & 2 deletions src/apps/beefy/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ const beefyBaseVaultsPositions = async ({
tvls: BeefyPrices
t: TFunction
}) => {
const client = getClient(networkId)
const client = getClient(networkId, 'beefy')

const userVaults: (BaseBeefyVault & { balance: bigint })[] = []

Expand Down Expand Up @@ -397,7 +397,7 @@ const beefyGovVaultsPositions = async (
multicallAddress: Address,
prices: BeefyPrices,
) => {
const client = getClient(networkId)
const client = getClient(networkId, 'beefy')

const userVaults: (GovVault & { balance: bigint })[] = []

Expand Down
2 changes: 1 addition & 1 deletion src/apps/beefy/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ const hook: ShortcutsHook = {
// amount in smallest unit
const amountToSupply = parseUnits(tokens[0].amount, tokenDecimals)

const client = getClient(networkId)
const client = getClient(networkId, 'beefy')

const approvedAllowanceForSpender = await client.readContract({
address: tokenAddress,
Expand Down
2 changes: 1 addition & 1 deletion src/apps/compound/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const hook: PositionsHook = {
return []
}

const client = getClient(networkId)
const client = getClient(networkId, 'compound')

const results = await client.readContract({
code: compoundMulticallBytecode,
Expand Down
2 changes: 1 addition & 1 deletion src/apps/curve/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export async function getPoolPositionDefinitions(
let consideredPools = pools
if (address) {
// call balanceOf to check if user has balance on a pool
const client = getClient(networkId)
const client = getClient(networkId, 'curve')
const result = await client.multicall({
contracts: pools.map(
(pool) =>
Expand Down
2 changes: 1 addition & 1 deletion src/apps/hedgey/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const hook: PositionsHook = {
}
const planNfts = await getHedgeyPlanNfts({ address })
const now = BigInt(Math.floor(new Date().getTime() / 1000))
const client = getClient(networkId)
const client = getClient(networkId, 'hedgey')
const positions: ContractPositionDefinition[] = await Promise.all(
planNfts.map(async (planNft) => {
const tokenVestingPlanContract = {
Expand Down
2 changes: 1 addition & 1 deletion src/apps/mento/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async function getVeMentoPositionDefinition(
return undefined
}

const client = getClient(networkId)
const client = getClient(networkId, 'mento')
const [mentoTokenAddress, decimals, locked, balance] = await client.multicall(
{
contracts: [
Expand Down
2 changes: 1 addition & 1 deletion src/apps/stake-dao/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ async function getVaultPositionDefinitions(
networkId: NetworkId,
address: Address | undefined,
): Promise<PositionDefinition[]> {
const client = getClient(networkId)
const client = getClient(networkId, 'stake-dao')

const vaultAddresses = SD_VAULT_ADDRESSES_BY_NETWORK_ID[networkId]
const vaultsData = await client.multicall({
Expand Down
7 changes: 5 additions & 2 deletions src/apps/uniswap/positions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Address } from 'viem'
import { Address, PublicClient } from 'viem'
import { getClient } from '../../runtime/client'
import { getTokenId } from '../../runtime/getTokenId'
import { NetworkId } from '../../types/networkId'
Expand Down Expand Up @@ -75,8 +75,11 @@ export async function getUniswapV3PositionDefinitions(
nftPositions: Address,
factory: Address,
imageUrl: string = 'https://raw.githubusercontent.com/valora-inc/dapp-list/ab12ab234b4a6e01eff599c6bd0b7d5b44d6f39d/assets/uniswap.png',
client?: PublicClient,
): Promise<ContractPositionDefinition[]> {
const client = getClient(networkId)
if (!client) {
client = getClient(networkId, 'uniswap')
}
const userPools = await client.readContract({
abi: userPositionsAbi,
address: userPositionsMulticall,
Expand Down
2 changes: 1 addition & 1 deletion src/apps/walletconnect/positions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async function getStakedWctPositionDefinition(
return undefined
}

const client = getClient(networkId)
const client = getClient(networkId, 'walletconnect')
const locks = await client.readContract({
address: wctStakingConfig.stakingAddress,
abi: stakingAbi,
Expand Down
99 changes: 99 additions & 0 deletions src/metrics/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import fs from 'fs'

export class Metrics {
networks: {
'celo-mainnet': number
'ethereum-mainnet': number
'arbitrum-one': number
'op-mainnet': number
'celo-alfajores': number
'ethereum-sepolia': number
'arbitrum-sepolia': number
'op-sepolia': number
'polygon-pos-mainnet': number
'polygon-pos-amoy': number
'base-mainnet': number
'base-sepolia': number
}
apps: {
aave: number
allbridge: number
beefy: number
compound: number
curve: number
gooddollar: number
halofi: number
hedgey: number
'locked-celo': number
mento: number
moola: number
'stake-dao': number
ubeswap: number
uniswap: number
walletconnect: number
}
appNames: string[]
networkNames: string[]

constructor() {
this.networks = {
'celo-mainnet': 0,
'ethereum-mainnet': 0,
'arbitrum-one': 0,
'op-mainnet': 0,
'celo-alfajores': 0,
'ethereum-sepolia': 0,
'arbitrum-sepolia': 0,
'op-sepolia': 0,
'polygon-pos-mainnet': 0,
'polygon-pos-amoy': 0,
'base-mainnet': 0,
'base-sepolia': 0,
}
this.apps = {
aave: 0,
allbridge: 0,
beefy: 0,
compound: 0,
curve: 0,
gooddollar: 0,
halofi: 0,
hedgey: 0,
'locked-celo': 0,
mento: 0,
moola: 0,
'stake-dao': 0,
ubeswap: 0,
uniswap: 0,
walletconnect: 0,
}
this.appNames = Object.keys(this.apps)
this.networkNames = Object.keys(this.networks)
}

updateApp(appName: string) {
if (appName && this.appNames.includes(appName)) {
// @ts-expect-error Only app names are allowed
this.apps[appName] += 1
}
}

updateNetwork(networkId: string) {
if (networkId && this.networkNames.includes(networkId)) {
// @ts-expect-error Only app names are allowed
this.networks[networkId] += 1
}
}

save() {
fs.writeFileSync(
'.metrics.json',
JSON.stringify({
apps: this.apps,
networks: this.networks,
}),
)
}
}

export const metrics = new Metrics()
42 changes: 40 additions & 2 deletions src/runtime/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Chain, createPublicClient, http, PublicClient } from 'viem'
import {
Chain,
createPublicClient,
http,
HttpTransport,
HttpTransportConfig,
PublicClient,
} from 'viem'
import {
arbitrum,
arbitrumSepolia,
Expand All @@ -15,6 +22,10 @@ import {
} from 'viem/chains'
import { NetworkId } from '../types/networkId'
import { getConfig } from '../config'
import { metrics } from '../metrics'
import { logger } from '../log'

const MULTICALL_ADDRESS = '0XCA11BDE05977B3631167028862BE2A173976CA11'

const networkIdToViemChain: Record<NetworkId, Chain> = {
[NetworkId['celo-mainnet']]: celo,
Expand All @@ -38,15 +49,42 @@ const clientsCache = new Map<

export function getClient(
networkId: NetworkId,
appName?: string,
): PublicClient<ReturnType<typeof http>, Chain> {
let client = clientsCache.get(networkId)
if (client) {
return client
}

const rpcUrl = getConfig().NETWORK_ID_TO_RPC_URL[networkId]
function loggingHttpTransport(
url?: string,
Comment on lines 50 to +60
Copy link
Member

Choose a reason for hiding this comment

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

Happy to see you're tackling this! 👍

One early feedback I see in this implementation is that the app won't be correctly reported if there are more than 1 client requested for a given network:

const client = getClient('celo-mainnet', 'ubeswap');

// then in another app
const client2 = getClient('celo-mainnet', 'locked-celo');

// client2 is actually the same as client, because we cache them
// so client2 calls will be counted as 'ubeswap' instead of 'locked-celo'

The reason we cache them is because we take advantage of viem clients batching: https://github.com/mobilestack-xyz/hooks/blob/67da2c869492664781f71f6e63f0d40fad747c02/src/runtime/client.ts#L50-L61

And this actually makes it somewhat less clear how to count RPC calls by app.
For instance: 2 apps make an RPC call, but the real final RPC is 1 multicall containing both of them.
i.e. 1 single HTTP request.

The important thing is how many RPC HTTP requests are being made.
I guess if there's no simple way to breakdown by app because of batching, we could omit that breakdown and just count the number of RPC HTTP requests by network.

config: HttpTransportConfig = {},
): HttpTransport {
return http(url, {
onFetchRequest: async (request) => {
const body = await request.json()
metrics.updateNetwork(networkId)
if (appName) {
metrics.updateApp(appName)
}
let message = `${networkId}: `
message += appName ? `[${appName}] ` : ''
message += `Call to ${body.params[0].to}`
if (body.params[0].to.toUpperCase() === MULTICALL_ADDRESS) {
message += `(multicall) `
}
console.log(message)
logger.trace({ msg: message })
metrics.save()
},
...config,
})
}

client = createPublicClient({
chain: networkIdToViemChain[networkId],
transport: http(rpcUrl),
transport: loggingHttpTransport(rpcUrl),
// This enables call batching via multicall
// meaning client.call, client.readContract, etc. will batch calls (using multicall)
// when the promises are scheduled in the same event loop tick (or within `wait` ms)
Expand Down
Loading