diff --git a/package-lock.json b/package-lock.json index ac2c7ac1..3ac0886c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,10 +4,11 @@ "requires": true, "packages": { "": { + "name": "bridges-server", "dependencies": { "@aws-sdk/client-lambda": "^3.637.0", "@aws-sdk/client-s3": "^3.637.0", - "@defillama/sdk": "^5.0.80", + "@defillama/sdk": "^5.0.94", "@graphql-typed-document-node/core": "^3.2.0", "@solana/web3.js": "^1.87.3", "async-retry": "^1.3.1", @@ -27,6 +28,7 @@ "devDependencies": { "@types/async-retry": "^1.4.8", "@types/aws-lambda": "^8.10.101", + "@types/js-yaml": "^4.0.9", "@types/node": "^18.6.4", "@types/node-fetch": "^2.6.2", "@types/retry": "^0.12.5", @@ -982,9 +984,9 @@ } }, "node_modules/@defillama/sdk": { - "version": "5.0.80", - "resolved": "https://registry.npmjs.org/@defillama/sdk/-/sdk-5.0.80.tgz", - "integrity": "sha512-XG1tm90rWmTnv55Du6cUHvSI3XVCSbVVjAHcDh9f2hOHuoKezx1zTjBrDUirYQ4yCCe2zl2g4QmUgD6JR96ibA==", + "version": "5.0.94", + "resolved": "https://registry.npmjs.org/@defillama/sdk/-/sdk-5.0.94.tgz", + "integrity": "sha512-XlR5Gawx/uQXvkWZN962DBGXz2+6W4L8NaDIJ3b04vkD2QRhBYAftTKZfHgsYqBh/dFCOGQxh5P0jM49sf8dFw==", "dependencies": { "@aws-sdk/client-s3": "^3.400.0", "@elastic/elasticsearch": "^8.13.1", @@ -3441,6 +3443,13 @@ "integrity": "sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==", "dev": true }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-buffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/json-buffer/-/json-buffer-3.0.0.tgz", diff --git a/package.json b/package.json index e83577aa..466362c5 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "bridges-server", "scripts": { "deploy:prod": "sls deploy --stage prod", "serve": "sls offline start", @@ -13,6 +14,7 @@ "devDependencies": { "@types/async-retry": "^1.4.8", "@types/aws-lambda": "^8.10.101", + "@types/js-yaml": "^4.0.9", "@types/node": "^18.6.4", "@types/node-fetch": "^2.6.2", "@types/retry": "^0.12.5", @@ -26,7 +28,7 @@ "dependencies": { "@aws-sdk/client-lambda": "^3.637.0", "@aws-sdk/client-s3": "^3.637.0", - "@defillama/sdk": "^5.0.80", + "@defillama/sdk": "^5.0.94", "@graphql-typed-document-node/core": "^3.2.0", "@solana/web3.js": "^1.87.3", "async-retry": "^1.3.1", diff --git a/src/adapters/hyperlane/index.ts b/src/adapters/hyperlane/index.ts new file mode 100644 index 00000000..99682717 --- /dev/null +++ b/src/adapters/hyperlane/index.ts @@ -0,0 +1,161 @@ +import { ethers } from "ethers"; +import { getProvider, setProvider } from "@defillama/sdk"; +import { LlamaProvider } from "@defillama/sdk/build/util/LlamaProvider"; +import providerList from "@defillama/sdk/build/providers.json"; +import { Chain } from "@defillama/sdk/build/general"; + +import { BridgeAdapter, PartialContractEventParams } from "../../helpers/bridgeAdapter.type"; +import { getTxDataFromEVMEventLogs } from "../../helpers/processTransactions"; + +import * as yaml from 'js-yaml'; + +const baseUri = "https://raw.githubusercontent.com/hyperlane-xyz/hyperlane-registry/main"; + +let metadata: Record; +let addresses: Record>; +const chainMapping: Record = {}; + +async function setUp(): Promise { + metadata = + (await fetch(`${baseUri}/chains/metadata.yaml`) + .then((r) => r.text()) + .then((t) => yaml.load(t)) as Record); + addresses = + (await fetch(`${baseUri}/chains/addresses.yaml`) + .then((r) => r.text()) + .then((t) => yaml.load(t)) as Record>); + + for (const [, chain] of Object.entries(metadata)) { + if (chain.isTestnet) continue; + if (chain.protocol != "ethereum") continue; + + const provider = getProvider(chain.name); + if (provider === null) { + for (const p in providerList) { + const data = (providerList as any)[p]; + if (data.chainId == chain.chainId) { + chainMapping[chain.name] = p; + setProvider(chain.name, getProvider(p)); + break; + } + } + } + } + // const missing = []; + // for (const key in metadata) { + // const chain = metadata[key]; + // if (chain.isTestnet) continue; + // if (chain.protocol != "ethereum") continue; + + // const provider = getProvider(chain.name); + // if (provider === null) missing.push(chain); + // } + // console.log(missing.length); + // console.log(missing); +}; + +function bytes32ToAddress(bytes32: string) { + return ethers.utils.getAddress('0x' + bytes32.slice(26)); +} + +function constructParams(chain: string) { + let eventParams = [] as PartialContractEventParams[]; + const mailboxAddress = ethers.utils.getAddress(addresses[chain].mailbox); + + function buildFilter(eventName: string): (provider: LlamaProvider, iface: ethers.utils.Interface, txHash: string) => Promise { + return async (provider, iface, txHash) => { + const txReceipt = await provider.getTransactionReceipt(txHash); + if (!txReceipt) return true; + + let toFilter = true; + txReceipt.logs.map((log: ethers.providers.Log) => { + let parsed; + try { + parsed = iface.parseLog(log); + } catch { return; } + // console.log(parsed); + // console.log(ethers.utils.getAddress(log.address)); + if (ethers.utils.getAddress(log.address) === mailboxAddress && parsed.name === eventName) { + toFilter = false; + } + }); + return toFilter; + } + } + + const commonParams = { + target: null, + logKeys: { + blockNumber: "blockNumber", + txHash: "transactionHash", + from: "address", + token: "address", + }, + argKeys: { + to: "recipient", + amount: "amount", + }, + argGetters: { + to: (log: any) => { bytes32ToAddress(log.recipient); } + }, + }; + const depositParams: PartialContractEventParams = { + ...commonParams, + topic: "SentTransferRemote(uint32,bytes32,uint256)", + abi: [ + "event SentTransferRemote(uint32 indexed destination, bytes32 indexed recipient, uint256 amount)", + "event Dispatch(address indexed sender, uint32 indexed destination, bytes32 indexed recipient, bytes message)", + ], + isDeposit: true, + filter: { + custom: buildFilter("Dispatch") + }, + }; + + const withdrawParams: PartialContractEventParams = { + ...commonParams, + topic: "ReceivedTransferRemote(uint32,bytes32,uint256)", + abi: [ + "event ReceivedTransferRemote(uint32 indexed origin, bytes32 indexed recipient, uint256 amount)", + "event Process(uint32 indexed origin, bytes32 indexed sender, address indexed recipient)", + ], + isDeposit: false, + filter: { + custom: buildFilter("Process") + }, + }; + + eventParams.push(depositParams, withdrawParams); + + return async (fromBlock: number, toBlock: number) => { + return await getTxDataFromEVMEventLogs("hyperlane", (chainMapping[chain] || chain) as Chain, fromBlock, toBlock, eventParams); + } +} + +const excludedChains = [ + "cheesechain", // TODO: not available in defillama sdk providerList, can be added manually + "lumia", // TODO: not available in defillama sdk providerList, can be added manually +]; + +async function build(): Promise { + await setUp(); + + const adapter: BridgeAdapter = { + } + + for (const key in metadata) { + const chain = metadata[key]; + if (chain.isTestnet) continue; + if (chain.protocol != "ethereum") continue; + + if (!addresses.hasOwnProperty(key)) continue; + if (excludedChains.includes(key)) continue; + + const adapterKey = chainMapping[key] || key; + adapter[adapterKey] = constructParams(key); + } + + return adapter; +} + +export default { isAsync: true, build }; diff --git a/src/adapters/index.ts b/src/adapters/index.ts index 8209247e..f13b4028 100644 --- a/src/adapters/index.ts +++ b/src/adapters/index.ts @@ -1,4 +1,4 @@ -import { BridgeAdapter } from "../helpers/bridgeAdapter.type"; +import { BridgeAdapter, AsyncBridgeAdapter } from "../helpers/bridgeAdapter.type"; import polygon from "./polygon"; import synapse from "./synapse"; import hop from "./hop"; @@ -71,6 +71,7 @@ import mint from "./mint"; import suibridge from "./suibridge"; import retrobridge from "./retrobridge" import layerswap from "./layerswap" +import hyperlane from "./hyperlane"; export default { polygon, @@ -145,6 +146,7 @@ export default { suibridge, retrobridge, layerswap, + hyperlane, } as { - [bridge: string]: BridgeAdapter; + [bridge: string]: BridgeAdapter | AsyncBridgeAdapter; }; diff --git a/src/adapters/test.ts b/src/adapters/test.ts index 6406599c..d2239e7a 100644 --- a/src/adapters/test.ts +++ b/src/adapters/test.ts @@ -4,6 +4,7 @@ import adapters from "./"; import { importBridgeNetwork } from "../data/importBridgeNetwork"; import { getLlamaPrices } from "../utils/prices"; import { transformTokens } from "../helpers/tokenMappings"; +import { isAsyncAdapter } from "../utils/adapter"; const logTypes = { txHash: "string", @@ -24,7 +25,8 @@ const adapterName = process.argv[2]; const numberOfBlocks = process.argv[3]; const testAdapter = async () => { - const adapter = adapters[adapterName]; + let adapter = adapters[adapterName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; if (!adapter) { throw new Error(`Adapter for ${adapterName} not found, check it is exported correctly.`); } diff --git a/src/data/bridgeNetworkData.ts b/src/data/bridgeNetworkData.ts index aa980ae7..c0af44c6 100644 --- a/src/data/bridgeNetworkData.ts +++ b/src/data/bridgeNetworkData.ts @@ -1608,4 +1608,84 @@ export default [ bitlayer: "btr", }, }, + { + id: 72, + displayName: "Hyperlane", + bridgeDbName: "hyperlane", + iconLink: "icons:hyperlane", + largeTxThreshold: 10000, + url: "https://hyperlane.xyz/", + chains: [ + "Aleph Zero EVM", + "Ancient8", + "Arbitrum", + "Arthera", + "Astar", + "Astar zkEVM", + "Avalanche", + "Base", + "Bitlayer", + "Blast", + "BOB", + "BSC", + "Celo", + // "CheeseChain", + "Chiliz", + "Core", + "Cyber", + "Degen", + "Dogechain", + "Endurance", + "Ethereum", + "Everclear", + "Flare", + "Forma", + "Fraxtal", + "Fuse", + "Gnosis", + "Immutable zkEVM", + "Injective EVM", + "KalyChain", + "Kroma", + "Linea", + "Lisk", + "LUKSO", + // "Lumia Prism", + "Manta", + "Mantle", + "Merlin", + "Metis", + "Mint", + "Mode", + "Molten", + "Moonbeam", + "Oort", + "Optimism", + "Polygon", + "Polygon zkEVM", + "Proof of Play Apex", + "PulseChain", + "RARI Chain", + "re.al", + "Redstone", + "Rootstock", + "Sanko", + "Scroll", + "Sei", + "Shibarium", + "Superposition", + "Taiko", + "Tangle", + "Viction", + "World Chain", + "Xai", + "X Layer", + "ZetaChain", + "Zircuit", + "Zora", + ], + chainMapping: { + avalanche: "avax", + }, + }, ] as BridgeNetwork[]; diff --git a/src/helpers/bridgeAdapter.type.ts b/src/helpers/bridgeAdapter.type.ts index 8490ead2..5e44d434 100644 --- a/src/helpers/bridgeAdapter.type.ts +++ b/src/helpers/bridgeAdapter.type.ts @@ -1,4 +1,6 @@ import { Chain } from "@defillama/sdk/build/general"; +import { LlamaProvider } from "@defillama/sdk/build/util/LlamaProvider"; +import { ethers } from "ethers"; import { EventKeyMapping } from "../utils/types"; import { EventData } from "../utils/types"; @@ -6,6 +8,11 @@ export type BridgeAdapter = { [chain: string]: (fromBlock: number, toBlock: number) => Promise; }; +export type AsyncBridgeAdapter = { + isAsync: boolean; + build: () => Promise; +}; + export type EventLogFilter = { includeToken?: string[]; includeFrom?: string[]; @@ -16,6 +23,7 @@ export type EventLogFilter = { includeArg?: { [key: string]: string }[]; excludeArg?: { [key: string]: string }[]; includeTxData?: { [key: string]: string }[]; + custom?: (provider: LlamaProvider, iface: ethers.utils.Interface, transactionHash: string) => Promise; }; export type FunctionSignatureFilter = { diff --git a/src/helpers/processTransactions.ts b/src/helpers/processTransactions.ts index 946f3c0f..9f90fba8 100644 --- a/src/helpers/processTransactions.ts +++ b/src/helpers/processTransactions.ts @@ -271,6 +271,10 @@ export const getTxDataFromEVMEventLogs = async ( if (toFilter) dataKeysToFilter.push(i); } } + if (filter?.custom) { + let toFilter = await filter.custom(provider, iface, txLog.transactionHash); + if (toFilter) dataKeysToFilter.push(i); + } if (getTokenFromReceipt && getTokenFromReceipt.token) { const txReceipt = await provider.getTransactionReceipt(txLog.transactionHash); if (!txReceipt) { diff --git a/src/utils/adapter.ts b/src/utils/adapter.ts index 023b1aff..6fb2caa5 100644 --- a/src/utils/adapter.ts +++ b/src/utils/adapter.ts @@ -7,7 +7,7 @@ import bridgeNetworks from "../data/bridgeNetworkData"; import adapters from "../adapters"; import { maxBlocksToQueryByChain, nonBlocksChains } from "./constants"; import { store } from "./s3"; -import { BridgeAdapter } from "../helpers/bridgeAdapter.type"; +import { BridgeAdapter, AsyncBridgeAdapter } from "../helpers/bridgeAdapter.type"; import { getCurrentUnixTimestamp } from "./date"; import type { RecordedBlocks } from "./types"; import { wait } from "../helpers/etherscan"; @@ -117,7 +117,8 @@ export const runAdapterToCurrentBlock = async ( console.warn(`[WARN] No recorded blocks data for ${bridgeDbName}. Error: ${e.message}`); } - const adapter = adapters[bridgeDbName]; + let adapter = adapters[bridgeDbName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; if (!adapter) { const errString = `Adapter for ${bridgeDbName} not found, check it is exported correctly.`; console.error(`[ERROR] ${errString}`); @@ -257,7 +258,8 @@ export const runAllAdaptersToCurrentBlock = async ( for (const bridgeNetwork of bridgeNetworks) { const { id, bridgeDbName } = bridgeNetwork; - const adapter = adapters[bridgeDbName]; + let adapter = adapters[bridgeDbName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; if (!adapter) { const errString = `Adapter for ${bridgeDbName} not found, check it is exported correctly.`; await insertErrorRow({ @@ -311,7 +313,8 @@ export const runAllAdaptersTimestampRange = async ( ) => { for (const bridgeNetwork of bridgeNetworks) { const { id, bridgeDbName } = bridgeNetwork; - const adapter = adapters[bridgeDbName]; + let adapter = adapters[bridgeDbName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; if (!adapter) { const errString = `Adapter for ${bridgeDbName} not found, check it is exported correctly.`; await insertErrorRow({ @@ -373,7 +376,8 @@ export const runAdapterHistorical = async ( const currentTimestamp = await getCurrentUnixTimestamp(); const bridgeNetwork = bridgeNetworks.filter((bridgeNetwork) => bridgeNetwork.id === bridgeNetworkId)[0]; const { bridgeDbName } = bridgeNetwork; - const adapter = adapters[bridgeDbName]; + let adapter = adapters[bridgeDbName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; console.log(`[INFO] Running adapter for ${bridgeDbName} on ${chain} from ${startBlock} to ${endBlock}.`); @@ -704,3 +708,7 @@ export const insertConfigEntriesForAdapter = async ( }) ); }; + +export function isAsyncAdapter(adapter: BridgeAdapter | AsyncBridgeAdapter): adapter is AsyncBridgeAdapter { + return (adapter as AsyncBridgeAdapter).isAsync; +} diff --git a/src/utils/aggregate.ts b/src/utils/aggregate.ts index 74315ddb..dac0c2ad 100644 --- a/src/utils/aggregate.ts +++ b/src/utils/aggregate.ts @@ -20,6 +20,7 @@ import { insertOrUpdateTokenWithoutPrice, } from "./wrappa/postgres/write"; import adapters from "../adapters"; +import { isAsyncAdapter } from "../utils/adapter"; import bridgeNetworks from "../data/bridgeNetworkData"; import { importBridgeNetwork } from "../data/importBridgeNetwork"; import { defaultConfidenceThreshold } from "./constants"; @@ -66,7 +67,8 @@ export const runAggregateDataHistorical = async ( } const { bridgeDbName, largeTxThreshold } = bridgeNetwork!; - const adapter = adapters[bridgeDbName]; + let adapter = adapters[bridgeDbName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; if (!adapter) { const errString = `Adapter for ${bridgeDbName} not found, check it is exported correctly.`; @@ -125,7 +127,8 @@ export const runAggregateDataAllAdapters = async (timestamp: number, hourly: boo .for(bridgeNetworks) .process(async (bridgeNetwork) => { const { bridgeDbName, largeTxThreshold } = bridgeNetwork; - const adapter = adapters[bridgeDbName]; + let adapter = adapters[bridgeDbName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; const chains = Object.keys(adapter); const chainsPromises = Promise.all( chains.map(async (chain) => { diff --git a/src/utils/insertConfigRows.ts b/src/utils/insertConfigRows.ts index d9824f1c..6c15a6f9 100644 --- a/src/utils/insertConfigRows.ts +++ b/src/utils/insertConfigRows.ts @@ -1,10 +1,12 @@ import { insertConfigEntriesForAdapter } from "./adapter"; import adapters from "../adapters"; +import { isAsyncAdapter } from "../utils/adapter"; async function insertConfigRows(bridgeDbName: string) { - const adapter = adapters[bridgeDbName]; + let adapter = adapters[bridgeDbName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; await insertConfigEntriesForAdapter(adapter, bridgeDbName) } -insertConfigRows("allbridge") \ No newline at end of file +insertConfigRows("allbridge") diff --git a/src/utils/insertRecordedBlocks.ts b/src/utils/insertRecordedBlocks.ts index bc6dc552..d04eab0a 100644 --- a/src/utils/insertRecordedBlocks.ts +++ b/src/utils/insertRecordedBlocks.ts @@ -1,5 +1,6 @@ import recordedBlocksRecord from "./recordedBlocks.json"; import adapters from "../adapters"; +import { isAsyncAdapter } from "../utils/adapter"; import { lookupBlock } from "@defillama/sdk/build/util"; import { Chain } from "@defillama/sdk/build/general"; import bridgeNetworks from "../data/bridgeNetworkData"; @@ -8,7 +9,8 @@ const FileSystem = require("fs"); const insertRecordedBlocks = async (adapterName: string, startTimestamp: number, endTimestamp: number) => { let recordedBlocks = recordedBlocksRecord as { [adapterChain: string]: { startBlock: number; endBlock: number } }; - const adapter = adapters[adapterName]; + let adapter = adapters[adapterName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; if (!adapter) { throw new Error(`Adapter for ${adapterName} not found, check it is exported correctly.`); } diff --git a/src/utils/testAdapterHistorical.ts b/src/utils/testAdapterHistorical.ts index 7e2b4fb6..321fbd69 100644 --- a/src/utils/testAdapterHistorical.ts +++ b/src/utils/testAdapterHistorical.ts @@ -4,6 +4,7 @@ import bridgeNetworkData from "../data/bridgeNetworkData"; import { wait } from "../helpers/etherscan"; import { maxBlocksToQueryByChain, nonBlocksChains } from "./constants"; import adapters from "../adapters"; +import { isAsyncAdapter } from "../utils/adapter"; import { getCurrentUnixTimestamp } from "./date"; import { getBlockByTimestamp } from "./blocks"; const retry = require("async-retry"); @@ -22,7 +23,8 @@ export const runAdapterHistorical = async ( const currentTimestamp = await getCurrentUnixTimestamp(); const bridgeNetwork = bridgeNetworkData.filter((bridgeNetwork) => bridgeNetwork.id === bridgeNetworkId)[0]; const { bridgeDbName } = bridgeNetwork; - const adapter = adapters[bridgeDbName]; + let adapter = adapters[bridgeDbName]; + adapter = isAsyncAdapter(adapter) ? await adapter.build() : adapter; if (!adapter) { const errString = `Adapter for ${bridgeDbName} not found, check it is exported correctly.`;