diff --git a/.github/workflows/process-issue.yml b/.github/workflows/process-issue.yml index 27c2e32a..465c31f8 100644 --- a/.github/workflows/process-issue.yml +++ b/.github/workflows/process-issue.yml @@ -39,6 +39,10 @@ jobs: SONIC_RPC_URL: ${{ secrets.SONIC_RPC_URL }} HYPEREVM_RPC_URL: ${{ secrets.HYPEREVM_RPC_URL }} PLASMA_RPC_URL: ${{ secrets.PLASMA_RPC_URL }} + XLAYER_API_KEY: ${{ secrets.XLAYER_API_KEY }} + XLAYER_SECRET_KEY: ${{ secrets.XLAYER_SECRET_KEY }} + XLAYER_PASSPHRASE: ${{ secrets.XLAYER_PASSPHRASE }} + XLAYER_RPC_URL: ${{ secrets.XLAYER_RPC_URL }} HYPERNATIVE_CLIENT_ID: ${{ secrets.HYPERNATIVE_CLIENT_ID }} HYPERNATIVE_CLIENT_SECRET: ${{ secrets.HYPERNATIVE_CLIENT_SECRET }} - name: Validate registry format diff --git a/README.md b/README.md index 8f0783d5..18f300d3 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Rate Provider Factories for reference | zkEVM | 0x4132f7AcC9dB7A6cF7BE2Dd3A9DC8b30C7E6E6c8 | N/A | | HyperEVM | 0x03362f847b4fabc12e1ce98b6b59f94401e4588e | 0xec2c6184761ab7fe061130b4a7e3da89c72f8395 | | Plasma | 0x138D9E0d0Cc4906C4cD865b38c9340A5CeDD9850 | 0x4E185b1502Fea7a06B63fDdA6de38F92C9528566 | +| Xlayer | 0x467665D4ae90e7A99c9C9AF785791058426d6eA0 | 0xeC2C6184761ab7fE061130B4A7e3Da89c72F8395 | Rate Transformer Factories Use these factories when an ERC4626 vault contains a yield bearing token to combine their resepctive rates of growth. This denominates the vault asset in the underlying for correlated pairs. For example Aave-wstETH pairing with Aave-wETH by denominating the assets in wETH. @@ -54,6 +55,8 @@ Use this factories to combine rate providers, similar to AaveRateTransformers. T | Optimism | 0x7d9507014cc564e3b95e4d0972a878d0862af7ae | | HyperEVM | 0x138d9e0d0cc4906c4cd865b38c9340a5cedd9850 | | Plasma | 0x470C9034F50afe6633f7e84A80B9961baa893d77 | +| Xlayer | 0x138D9E0d0Cc4906C4cD865b38c9340A5CeDD9850 | + --- ## Setup diff --git a/scripts/process-issue.ts b/scripts/process-issue.ts index b1865333..b9d63bb4 100644 --- a/scripts/process-issue.ts +++ b/scripts/process-issue.ts @@ -17,7 +17,7 @@ import { polygonZkEvm, mode, } from 'viem/chains' -import { hyperEvm, plasma } from '../src/utils/customChains' +import { hyperEvm, plasma, xlayer } from '../src/utils/customChains' import { Hex } from 'viem' /* @@ -70,11 +70,12 @@ async function processIssue(issueJson: string) { optimism, sonic, sepolia, - polygon, + polygon, polygonZkEvm, mode, hyperEvm, plasma, + xlayer, } let network = networks[issueData.network] @@ -101,7 +102,9 @@ async function processIssue(issueJson: string) { rpcUrl as string, issueData.protocol_documentation, issueData.audits, - issueData.additional_contract_information.selected.includes('Is the rate provider reporting a market rate?') ? { isMarketRate: true } : undefined, + issueData.additional_contract_information.selected.includes('Is the rate provider reporting a market rate?') + ? { isMarketRate: true } + : undefined, ) // this step requires the registry to be read thus having the registry updated already diff --git a/scripts/write-erc4626-review.ts b/scripts/write-erc4626-review.ts index 02aa9d8d..78f87d29 100644 --- a/scripts/write-erc4626-review.ts +++ b/scripts/write-erc4626-review.ts @@ -19,7 +19,7 @@ import { mode, } from 'viem/chains' -import { hyperEvm, plasma } from '../src/utils/customChains' +import { hyperEvm, plasma, xlayer } from '../src/utils/customChains' import { writeReviewAndUpdateRegistry } from '../src/utils/write-erc4626-review' dotenv.config() @@ -42,7 +42,19 @@ async function main() { alias: 'n', type: 'string', description: 'The network the rate provider is deployed on', - choices: ['base', 'mainnet', 'arbitrum', 'avalanche', 'gnosis', 'fraxtal', 'optimism', 'sonic', 'hyperEvm', 'plasma'], + choices: [ + 'base', + 'mainnet', + 'arbitrum', + 'avalanche', + 'gnosis', + 'fraxtal', + 'optimism', + 'sonic', + 'hyperEvm', + 'plasma', + 'xlayer', + ], demandOption: true, }) .option('rpcUrl', { @@ -67,7 +79,8 @@ async function main() { polygonZkEvm, mode, hyperEvm, - plasma + plasma, + xlayer, } let network = networks[argv.network] diff --git a/scripts/write-review.ts b/scripts/write-review.ts index 009c71b0..68795476 100644 --- a/scripts/write-review.ts +++ b/scripts/write-review.ts @@ -20,7 +20,7 @@ import { mode, } from 'viem/chains' -import { hyperEvm, plasma } from '../src/utils/customChains' +import { hyperEvm, plasma, xlayer } from '../src/utils/customChains' import { writeReviewAndUpdateRegistry } from '../src/utils/write-rp-review' dotenv.config() @@ -44,7 +44,19 @@ async function main() { alias: 'n', type: 'string', description: 'The network the rate provider is deployed on', - choices: ['base', 'mainnet', 'arbitrum', 'avalanche', 'gnosis', 'fraxtal', 'optimism', 'sonic', 'hyperEvm', 'plasma'], + choices: [ + 'base', + 'mainnet', + 'arbitrum', + 'avalanche', + 'gnosis', + 'fraxtal', + 'optimism', + 'sonic', + 'hyperEvm', + 'plasma', + 'xlayer', + ], demandOption: true, }) .option('rateProviderAsset', { @@ -75,7 +87,8 @@ async function main() { polygonZkEvm, mode, hyperEvm, - plasma + plasma, + xlayer, } let network = networks[argv.network] diff --git a/src/app.ts b/src/app.ts index 39ef3e7b..564f83ba 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,8 +2,10 @@ import { Address, parseEventLogs } from 'viem' import { CreateAccessListReturnType } from 'viem/_types/actions/public/createAccessList' import { createPublicClient, encodeFunctionData, http } from 'viem' import { Chain, Hex, Abi } from 'viem' -import EtherscanApi from './services/etherscanApi' import { rateProviderAbi } from './utils/abi/rateProvider' +import { xlayer } from './utils/customChains' +import { ChainApi } from './types/types' +import { createChainApi } from './utils/factories' class RateProviderDataService { public rateProvider: Address @@ -22,6 +24,7 @@ class RateProviderDataService { } private apiKey!: string + private chainApi!: ChainApi public tenderlySettings!: { accountSlug: string projectSlug: string @@ -32,6 +35,7 @@ class RateProviderDataService { this.rateProvider = rateProvider this.chain = chain this.setApiBasedOnChain(chain) + this.chainApi = createChainApi(chain) this.tenderlySettings = { accountSlug: process.env.TENDERLY_ACCOUNT_SLUG || @@ -89,8 +93,6 @@ class RateProviderDataService { * @returns An array of deployment information. */ public async getDeploymentBlocks(addresses: Address[]): Promise<{ address: Address; deploymentTxHash: Hex }[]> { - const etherscanApi = new EtherscanApi(this.chain, this.apiKey) - // The addresses length can be arbitrary but the etherscan API only allows 5 addresses at a time // so we need to chunk the addresses and call the API sequentially const chunkSize = 5 @@ -98,7 +100,7 @@ class RateProviderDataService { for (let i = 0; i < addresses.length; i += chunkSize) { const chunk = addresses.slice(i, i + chunkSize) - const chunkTxHashes = await etherscanApi.getDeploymentTxHashAndBlock(chunk) + const chunkTxHashes = await this.chainApi.getDeploymentTxHashAndBlock(chunk) txHashes.push(...chunkTxHashes) } @@ -113,8 +115,7 @@ class RateProviderDataService { public async getContractInfo( addresses: Address[], ): Promise<{ address: Address; Proxy: string; ContractName: string; ABI: string; Implementation: Address }[]> { - const etherscanApi = new EtherscanApi(this.chain, this.apiKey) - return await etherscanApi.getSourceCode(addresses) + return await this.chainApi.getSourceCode(addresses) } /** @@ -136,8 +137,11 @@ class RateProviderDataService { allContractAddresses.push(this.rateProvider) } + // get info on all contracts in the access list const proxiesWithRateProvider = await this.getContractInfo(allContractAddresses) + // filter out the contracts that are not proxies const filteredProxiesList = proxiesWithRateProvider.filter((p) => p.Proxy === '1') + const proxiesWithRateProviderDeploymentInfo = await this.getDeploymentBlocks( filteredProxiesList.map((p) => p.address), ) @@ -164,21 +168,21 @@ class RateProviderDataService { const proxiesWithRateProviderCompleteInfo = await Promise.all( receipts.map(async (info) => { try { - if (!info.ABI) { - throw new Error(`ABI not provided for contract at address ${info.address}`) - } - const events = parseEventLogs({ - logs: info.receipt.logs, - abi: JSON.parse(info.ABI), - eventName: 'Upgraded', - }) - const wasUpgraded = events.length > 0 - - return { ...info, events, wasUpgraded } - } catch (error) { - console.error(`Error processing contract at address ${info.address}:`, error) - return { ...info, events: [], wasUpgraded: false, error } - } + if (!info.ABI) { + throw new Error(`ABI not provided for contract at address ${info.address}`) + } + const events = parseEventLogs({ + logs: info.receipt.logs, + abi: JSON.parse(info.ABI), + eventName: 'Upgraded', + }) + const wasUpgraded = events.length > 0 + + return { ...info, events, wasUpgraded } + } catch (error) { + console.error(`Error processing contract at address ${info.address}:`, error) + return { ...info, events: [], wasUpgraded: false, error } + } }), ) @@ -211,8 +215,7 @@ class RateProviderDataService { public async getContractSourceCode( address: Address, ): Promise<{ address: Address; Proxy: string; ContractName: string; ABI: string; Implementation: Address }> { - const etherscanApi = new EtherscanApi(this.chain, this.apiKey) - const sourceCodeArray = await etherscanApi.getSourceCode([address]) + const sourceCodeArray = await this.chainApi.getSourceCode([address]) return sourceCodeArray[0] } @@ -320,7 +323,7 @@ class RateProviderDataService { return ( item.type === 'function' && item.name === 'getRate' && - item.stateMutability === 'view' && + (item.stateMutability === 'view' || item.stateMutability === 'pure') && item.outputs?.length === 1 && item.outputs[0].internalType === 'uint256' ) @@ -338,6 +341,9 @@ class RateProviderDataService { : (() => { throw new Error(`ETHERSCAN_API_KEY Environment variable is not set`) })() + if (chain.id === xlayer.id) { + this.apiKey = process.env.XLAYER_API_KEY || '' + } } } diff --git a/src/services/etherscanApi.ts b/src/services/etherscanApi.ts index 9def387f..88cd1611 100644 --- a/src/services/etherscanApi.ts +++ b/src/services/etherscanApi.ts @@ -2,13 +2,14 @@ import { Address, Hex, Chain } from 'viem' import { TransactionData, GetContractSourceCodeResponse } from '../types/types' import { avalanche } from 'viem/chains' import { plasma } from '../utils/customChains' +import { ChainApi } from '../types/types' /** * EtherscanApi class to interact with Etherscan API * It uses the Etherscan API 2.0 * Supported chains can be found at https://docs.etherscan.io/etherscan-v2/supported-chains */ -class EtherscanApi { +class EtherscanApi implements ChainApi { public chain: Chain private apiKey: string @@ -44,7 +45,10 @@ class EtherscanApi { const snowtraceResponse = await fetch(snowtraceUrl) if (snowtraceResponse.ok) { const fallbackData = await snowtraceResponse.json() - if (fallbackData.status === '1' && fallbackData.result[0].ABI !== 'Contract source code not verified') { + if ( + fallbackData.status === '1' && + fallbackData.result[0].ABI !== 'Contract source code not verified' + ) { console.log(`Successfully found contract on Snowscan for address ${address}`) return fallbackData } @@ -93,7 +97,7 @@ class EtherscanApi { // the contract can be unverified if (data.result[0].ABI === 'Contract source code not verified') { console.log(`Contract is unverified for address ${address}, trying fallback block explorer`) - + // Try fallback explorer (e.g., Snowscan for Avalanche) const fallbackData = await this.tryFallbackExplorer(this.chain, address) if (fallbackData) { @@ -101,7 +105,7 @@ class EtherscanApi { results.push({ address, Proxy, ContractName, ABI, Implementation }) continue } - + console.log(`Contract is unverified on all explorers for address ${address}`) continue // Skip this address } diff --git a/src/services/xlayerApi.ts b/src/services/xlayerApi.ts new file mode 100644 index 00000000..3bb11e2a --- /dev/null +++ b/src/services/xlayerApi.ts @@ -0,0 +1,149 @@ +import { Address, Hex, Chain } from 'viem' +import { ChainApi, XLayerContractInfo, XLayerAddressInfo } from '../types/types' +import crypto from 'crypto' + +/** + * XLayerApi class to interact with XLayer Chain Explorer API + * This API provides contract source code and deployment information for XLayer Chain. + * API documentation: https://web3.okx.com/xlayer/onchaindata/docs/en/#introduction/ + */ +class XLayerApi implements ChainApi { + public chain: Chain + private apiKey: string + private secretKey: string // NEW: Required for HMAC signing + private passphrase: string // NEW: Required for authentication + private readonly baseUrl = 'https://www.okx.com/api/v5/xlayer' + + constructor(chain: Chain, apiKey: string, secretKey: string, passphrase: string) { + this.chain = chain + this.apiKey = apiKey + this.secretKey = secretKey + this.passphrase = passphrase + } + + /** + * Fetches data from the XLayer API with proper authentication. + */ + private async fetchFromApi(url: string, method: string = 'GET', body: string = ''): Promise { + const timestamp = new Date().toISOString() + const urlObj = new URL(url) + const requestPath = urlObj.pathname + urlObj.search + const message = timestamp + method + requestPath + body + const signature = crypto.createHmac('sha256', this.secretKey).update(message).digest('base64') + + const response = await fetch(url, { + method, + headers: { + 'OK-ACCESS-KEY': this.apiKey, + 'OK-ACCESS-TIMESTAMP': timestamp, + 'OK-ACCESS-PASSPHRASE': this.passphrase, + 'OK-ACCESS-SIGN': signature, + 'Content-Type': 'application/json', + }, + body: body || undefined, + }) + + if (!response.ok) { + throw new Error(`Error fetching data from XLayer API: ${response.statusText} for ${url}`) + } + + const data = await response.json() + if (data.code !== '0') { + throw new Error(`XLayer API error: ${data.msg || 'Unknown error'} (code: ${data.code})`) + } + + return data + } + + /** + * Fetches source code information for the given addresses. + * Implements ChainApi interface. + */ + public async getSourceCode( + addresses: Address[], + ): Promise<{ address: Address; Proxy: string; ContractName: string; ABI: string; Implementation: Address }[]> { + const results = [] + + for (const address of addresses) { + try { + const url = `${this.baseUrl}/contract/verify-contract-info?chainShortName=XLAYER&contractAddress=${address}` + const response = await this.fetchFromApi(url) + + if (!response.data || response.data.length === 0) { + console.error(`Error fetching contract info for address ${address}: No data returned`) + continue + } + + const contractData: XLayerContractInfo = response.data[0] + if (!contractData.contractAbi) { + console.error(`ABI is missing for address ${address}`) + continue + } + + const { proxy, contractName, contractAbi, implementation } = contractData + results.push({ + address, + Proxy: proxy, + ContractName: contractName, + ABI: contractAbi, + Implementation: implementation as Address, + }) + } catch (error) { + console.error(`Error processing address ${address}:`, error) + } + + await this.delay(1000) + } + + return results + } + + /** + * Fetches deployment transaction hash and block number for the given addresses. + */ + public async getDeploymentTxHashAndBlock( + addresses: Address[], + ): Promise<{ address: Address; deploymentTxHash: Hex; blockNumber: string }[]> { + const results = [] + + for (const address of addresses) { + try { + const url = `${this.baseUrl}/address/information-evm?chainShortName=XLAYER&address=${address}` + const response = await this.fetchFromApi(url) + + if (!response.data || response.data.length === 0) { + console.error(`No deployment data found for address ${address}`) + continue + } + + const addressData: XLayerAddressInfo = response.data[0] + const deploymentTxHash = addressData.createContractTransactionHash + if (!deploymentTxHash) { + console.error(`No deployment transaction hash found for address ${address}`) + continue + } + + results.push({ + address, + deploymentTxHash: deploymentTxHash as Hex, + blockNumber: '', + }) + } catch (error) { + console.error(`Error processing deployment info for address ${address}:`, error) + } + + await this.delay(1000) + } + + return results + } + + /** + * Adds a delay between API calls to avoid rate limiting. + */ + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)) + } +} + +export default XLayerApi diff --git a/src/types/types.ts b/src/types/types.ts index 88a4b6a5..8c965bc8 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -29,6 +29,45 @@ export interface TransactionData { }> } +export interface XLayerContractInfo { + sourceCode: string + contractName: string + compilerVersion: string + optimization: string + optimizationRuns: string + contractAbi: string + evmVersion: string + licenseType: string + libraryInfo: Array<{ + libraryName: string + libraryAddress: string + }> + proxy: string + implementation: string + swarmSource: string +} + +export interface XLayerApiResponse { + code: string + msg: string + data: T[] +} + +export interface XLayerAddressInfo { + address: string + balance: string + balanceSymbol: string + transactionCount: string + firstTransactionTime: string + lastTransactionTime: string + contractAddress: boolean + createContractAddress: string + createContractTransactionHash: string + contractCorrespondingToken: string + contractCalls: string + contractCallingAddresses: string +} + export interface CustomAgentInput { chain: Chain ruleString: string @@ -48,3 +87,31 @@ export interface CustomAgentInputUpgrade { agentName: string operands?: string } + +/** + * Interface for chain explorer APIs that provide contract source code + * and deployment information. + */ +export interface ChainApi { + /** + * The chain this API instance is configured for. + */ + readonly chain: Chain + /** + * Fetches source code information for the given addresses. + * @param addresses The addresses to fetch source code for. + * @returns An array of contract information including ABI, proxy status, etc. + */ + getSourceCode( + addresses: Address[], + ): Promise<{ address: Address; Proxy: string; ContractName: string; ABI: string; Implementation: Address }[]> + + /** + * Fetches deployment transaction hash and block number for the given addresses. + * @param addresses The addresses to fetch deployment information for. + * @returns An array of deployment information. + */ + getDeploymentTxHashAndBlock( + addresses: Address[], + ): Promise<{ address: Address; deploymentTxHash: Hex; blockNumber: string }[]> +} diff --git a/src/utils/customChains.ts b/src/utils/customChains.ts index b95177a4..9642e766 100644 --- a/src/utils/customChains.ts +++ b/src/utils/customChains.ts @@ -56,4 +56,33 @@ export const plasma = /*#__PURE__*/ defineChain({ blockCreated: 1, }, }, -}) \ No newline at end of file +}) + +export const xlayer = /*#__PURE__*/ defineChain({ + id: 196, + name: 'XLayer', + nativeCurrency: { + name: 'XLAYER', + symbol: 'XLAYER', + decimals: 18, + }, + rpcUrls: { + default: { + http: ['https://rpc.xlayer.tech/'], + webSocket: ['wss://rpc.xlayer.tech/'], + }, + }, + blockExplorers: { + default: { + name: 'XLayer Explorer', + url: 'https://xlayer.io/', + apiUrl: 'https://api.xlayer.io/v1/api?', + }, + }, + contracts: { + multicall3: { + address: '0xcA11bde05977b3631167028862bE2a173976CA11', + blockCreated: 1, + }, + }, +}) diff --git a/src/utils/factories.ts b/src/utils/factories.ts new file mode 100644 index 00000000..2796fa86 --- /dev/null +++ b/src/utils/factories.ts @@ -0,0 +1,16 @@ +import { Chain } from 'viem' +import EtherscanApi from '../services/etherscanApi' +import XLayerApi from '../services/xlayerApi' +import { xlayer } from './customChains' + +export const createChainApi = (chain: Chain) => { + if (chain.id === xlayer.id) { + return new XLayerApi( + chain, + process.env.XLAYER_API_KEY || '', + process.env.XLAYER_SECRET_KEY || '', + process.env.XLAYER_PASSPHRASE || '', + ) + } + return new EtherscanApi(chain, process.env.ETHERSCAN_API_KEY || '') +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 0e163806..7e33a4e1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,3 +5,4 @@ export * from './onchainCallHelpers' export * from './validateInputData' export * from './write-rp-review' export * from './createCustomAgents' +export * from './factories' diff --git a/test/typescript/etherscanApi.test.ts b/test/typescript/etherscanApi.test.ts index 250e59b6..3794d35e 100644 --- a/test/typescript/etherscanApi.test.ts +++ b/test/typescript/etherscanApi.test.ts @@ -1,5 +1,4 @@ import { config } from 'dotenv' -import EtherscanApi from '../../src/services/etherscanApi' import { base, @@ -17,9 +16,9 @@ import { mode, } from 'viem/chains' -import { hyperEvm } from '../../src/utils/customChains' -import { plasma } from '../../src/utils/customChains' - +import { hyperEvm, plasma, xlayer } from '../../src/utils/customChains' +import { ChainApi } from '../../src/types/types' +import { createChainApi } from '../../src/utils/factories' // Check that the viem Chain has a multicall3 contract defined describe('test networks', () => { // Test data with different configurations @@ -36,6 +35,7 @@ describe('test networks', () => { optimism, hyperEvm, plasma, + xlayer, ] config() @@ -44,18 +44,18 @@ describe('test networks', () => { testNetworks.forEach((chain) => { describe(`when using ${chain.name}`, () => { jest.setTimeout(50000) - let etherscanApi: EtherscanApi + let chainApi: ChainApi beforeEach(() => { - etherscanApi = new EtherscanApi(chain, apiKey) + chainApi = createChainApi(chain) }) it('should get source code', async () => { - const [{ address, Proxy, ContractName, ABI, Implementation }] = await etherscanApi.getSourceCode([ - etherscanApi.chain.contracts?.multicall3?.address || '0x', + const [{ address, Proxy, ContractName, ABI, Implementation }] = await chainApi.getSourceCode([ + chainApi.chain.contracts?.multicall3?.address || '0x', ]) - expect(address).toEqual(etherscanApi.chain.contracts?.multicall3?.address || '0x') + expect(address).toEqual(chainApi.chain.contracts?.multicall3?.address || '0x') expect(Proxy).toEqual('0') expect(ContractName).toBeDefined() expect(ABI).toBeTruthy() @@ -63,10 +63,10 @@ describe('test networks', () => { }) it('should get deployment tx hash and block', async () => { - const [{ address, deploymentTxHash, blockNumber }] = await etherscanApi.getDeploymentTxHashAndBlock([ - etherscanApi.chain.contracts?.multicall3?.address || '0x', + const [{ address, deploymentTxHash, blockNumber }] = await chainApi.getDeploymentTxHashAndBlock([ + chainApi.chain.contracts?.multicall3?.address || '0x', ]) - expect(address).toEqual(etherscanApi.chain.contracts?.multicall3?.address || '0x') + expect(address).toEqual(chainApi.chain.contracts?.multicall3?.address || '0x') expect(deploymentTxHash).toBeTruthy() }) })