diff --git a/web/packages/api/src/history_v2.ts b/web/packages/api/src/history_v2.ts new file mode 100644 index 0000000000..b81bf9acf8 --- /dev/null +++ b/web/packages/api/src/history_v2.ts @@ -0,0 +1,254 @@ +import { + fetchToPolkadotTransfers, + fetchToEthereumTransfers, + fetchBridgeHubOutboundMessageAccepted, + fetchEthereumInboundMessageDispatched, + fetchBridgeHubInboundMessageReceived, + fetchMessageProcessedOnPolkadot, +} from "./subsquid" +import { getEventIndex } from "./utils" + +export enum TransferStatus { + Pending, + Complete, + Failed, +} + +export type TransferInfo = { + when: Date + sourceAddress: string + beneficiaryAddress: string + tokenAddress: string + destinationParachain?: number + destinationFee?: string + amount: string +} + +export type ToPolkadotTransferResult = { + id: string + status: TransferStatus + info: TransferInfo + submitted: { + blockHash: string + blockNumber: number + logIndex: number + transactionHash: string + transactionIndex: number + channelId: string + messageId: string + nonce: number + parentBeaconSlot?: number + } + beaconClientIncluded?: { + extrinsic_index: string + extrinsic_hash: string + event_index: string + block_timestamp: number + beaconSlot: number + beaconBlockHash: string + } + inboundMessageReceived?: { + extrinsic_index: string + extrinsic_hash: string + event_index: string + block_timestamp: number + messageId: string + channelId: string + nonce: number + } + assetHubMessageProcessed?: { + extrinsic_hash: string + event_index: string + block_timestamp: number + success: boolean + sibling: number + } +} + +export type ToEthereumTransferResult = { + id: string + status: TransferStatus + info: TransferInfo + submitted: { + extrinsic_index: string + extrinsic_hash: string + block_hash: string + account_id: string + block_num: number + block_timestamp: number + messageId: string + bridgeHubMessageId: string + success: boolean + relayChain?: { + block_hash: string + block_num: number + } + } + bridgeHubXcmDelivered?: { + extrinsic_hash: string + event_index: string + block_timestamp: number + siblingParachain: number + success: boolean + } + bridgeHubChannelDelivered?: { + extrinsic_hash: string + event_index: string + block_timestamp: number + channelId: string + success: boolean + } + bridgeHubMessageQueued?: { + extrinsic_hash: string + event_index: string + block_timestamp: number + } + bridgeHubMessageAccepted?: { + extrinsic_hash: string + event_index: string + block_timestamp: number + nonce: number + } + ethereumBeefyIncluded?: { + blockNumber: number + blockHash: string + transactionHash: string + transactionIndex: number + logIndex: number + relayChainblockNumber: number + mmrRoot: string + } + ethereumMessageDispatched?: { + blockNumber: number + blockHash: string + transactionHash: string + transactionIndex: number + logIndex: number + messageId: string + channelId: string + nonce: number + success: boolean + } +} + +export const toPolkadotHistory = async (): Promise => { + const ethOutboundMessages = await fetchToPolkadotTransfers() + const results: ToPolkadotTransferResult[] = [] + for (const outboundMessage of ethOutboundMessages) { + const result: ToPolkadotTransferResult = { + id: outboundMessage.id, + status: TransferStatus.Pending, + info: { + when: new Date(outboundMessage.timestamp), + sourceAddress: outboundMessage.senderAddress, + beneficiaryAddress: outboundMessage.destinationAddress, + tokenAddress: outboundMessage.tokenAddress, + destinationParachain: outboundMessage.destinationParaId, + destinationFee: "", + amount: outboundMessage.amount, + }, + submitted: { + blockHash: "", + blockNumber: outboundMessage.blockNumber, + logIndex: 0, + transactionHash: outboundMessage.txHash, + transactionIndex: 0, + channelId: outboundMessage.channelId, + messageId: outboundMessage.messageId, + nonce: outboundMessage.nonce, + }, + } + let inboundMessageReceived = await fetchBridgeHubInboundMessageReceived(result.id) + if (inboundMessageReceived) { + result.inboundMessageReceived = { + extrinsic_index: "", + extrinsic_hash: "", + event_index: getEventIndex(inboundMessageReceived.id), + block_timestamp: inboundMessageReceived.timestamp, + messageId: inboundMessageReceived.messageId, + channelId: inboundMessageReceived.channelId, + nonce: inboundMessageReceived.nonce, + } + } + + const assetHubMessageProcessed = await fetchMessageProcessedOnPolkadot(result.id) + if (assetHubMessageProcessed) { + result.assetHubMessageProcessed = { + extrinsic_hash: "", + event_index: getEventIndex(assetHubMessageProcessed.id), + block_timestamp: assetHubMessageProcessed.timestamp, + success: assetHubMessageProcessed.success, + sibling: 0, + } + if (!result.assetHubMessageProcessed.success) { + result.status = TransferStatus.Failed + continue + } + + result.status = TransferStatus.Complete + } + + results.push(result) + } + return results +} + +export const toEthereumHistory = async (): Promise => { + const allTransfers = await fetchToEthereumTransfers() + const results: ToEthereumTransferResult[] = [] + for (const transfer of allTransfers) { + const result: ToEthereumTransferResult = { + id: transfer.id, + status: TransferStatus.Pending, + info: { + when: new Date(transfer.timestamp), + sourceAddress: transfer.senderAddress, + tokenAddress: transfer.tokenAddress, + beneficiaryAddress: transfer.destinationAddress, + amount: transfer.amount, + }, + submitted: { + extrinsic_index: "", + extrinsic_hash: transfer.txHash, + block_hash: "", + account_id: transfer.senderAddress, + block_num: transfer.blockNumber, + block_timestamp: transfer.timestamp, + messageId: transfer.id, + bridgeHubMessageId: "", + success: true, + }, + } + + let outboundQueueAccepted = await fetchBridgeHubOutboundMessageAccepted(transfer.id) + if (outboundQueueAccepted) { + result.bridgeHubMessageQueued = { + block_timestamp: outboundQueueAccepted.timestamp, + event_index: getEventIndex(outboundQueueAccepted.id), + extrinsic_hash: "", + } + } + + let ethereumMessageDispatched = await fetchEthereumInboundMessageDispatched(transfer.id) + if (ethereumMessageDispatched) { + result.ethereumMessageDispatched = { + blockNumber: ethereumMessageDispatched.blockNumber, + blockHash: "", + transactionHash: ethereumMessageDispatched.txHash, + transactionIndex: 0, + logIndex: 0, + messageId: ethereumMessageDispatched.messageId, + channelId: ethereumMessageDispatched.channelId, + nonce: ethereumMessageDispatched.nonce, + success: ethereumMessageDispatched.success, + } + if (!result.ethereumMessageDispatched.success) { + result.status = TransferStatus.Failed + continue + } + result.status = TransferStatus.Complete + } + results.push(result) + } + return results +} diff --git a/web/packages/api/src/index.ts b/web/packages/api/src/index.ts index 22108da865..6b853d0534 100644 --- a/web/packages/api/src/index.ts +++ b/web/packages/api/src/index.ts @@ -175,3 +175,5 @@ export * as assets from "./assets" export * as environment from "./environment" export * as subscan from "./subscan" export * as history from "./history" +export * as historyV2 from "./history_v2" +export * as subsquid from "./subsquid" diff --git a/web/packages/api/src/subsquid.ts b/web/packages/api/src/subsquid.ts new file mode 100644 index 0000000000..3bb8f54de8 --- /dev/null +++ b/web/packages/api/src/subsquid.ts @@ -0,0 +1,125 @@ +const graphqlApiUrl = process.env["GRAPHQL_API_URL"] || "https://data.snowbridge.network/graphql" +const graphqlQuerySize = process.env["GRAPHQL_QUERY_SIZE"] || "100" + +export const fetchToPolkadotTransfers = async () => { + let query = `query { transferStatusToPolkadots(limit: ${graphqlQuerySize}, orderBy: blockNumber_DESC) { + id + status + blockNumber + bridgedBlockNumber + channelId + destinationAddress + destinationBlockNumber + destinationParaId + forwardedBlockNumber + messageId + nonce + senderAddress + timestamp + tokenAddress + txHash + amount + } + }` + let result = await queryByGraphQL(query) + return result.transferStatusToPolkadots +} + +export const fetchToEthereumTransfers = async () => { + let query = `query { transferStatusToEthereums(limit: ${graphqlQuerySize}, orderBy: blockNumber_DESC) { + id + status + blockNumber + bridgedBlockNumber + channelId + destinationAddress + destinationBlockNumber + forwardedBlockNumber + messageId + nonce + senderAddress + sourceParaId + timestamp + tokenAddress + txHash + amount + } + }` + let result = await queryByGraphQL(query) + return result.transferStatusToEthereums +} + +export const fetchBridgeHubOutboundMessageAccepted = async (messageID: string) => { + let query = `query { outboundMessageAcceptedOnBridgeHubs(where: {messageId_eq:"${messageID}"}) { + id + nonce + blockNumber + } + }` + let result = await queryByGraphQL(query) + return result?.outboundMessageAcceptedOnBridgeHubs[0] +} + +export const fetchEthereumInboundMessageDispatched = async (messageID: string) => { + let query = `query {inboundMessageDispatchedOnEthereums(where: {messageId_eq: "${messageID}"}) { + id + channelId + blockNumber + messageId + nonce + success + timestamp + txHash + } + }` + let result = await queryByGraphQL(query) + return result?.inboundMessageDispatchedOnEthereums[0] +} + +export const fetchBridgeHubInboundMessageReceived = async (messageID: string) => { + let query = `query { inboundMessageReceivedOnBridgeHubs(where: {messageId_eq:"${messageID}"}) { + id + channelId + blockNumber + messageId + nonce + timestamp + } + }` + let result = await queryByGraphQL(query) + return result?.inboundMessageReceivedOnBridgeHubs[0] +} + +export const fetchMessageProcessedOnPolkadot = async (messageID: string) => { + let query = `query { messageProcessedOnPolkadots(where: {messageId_eq:"${messageID}"}) { + id + blockNumber + messageId + paraId + timestamp + success + } + }` + let result = await queryByGraphQL(query) + return result?.messageProcessedOnPolkadots[0] +} + +export const fetchEstimatedDeliveryTime = async (channelId: string) => { + let query = `query { toEthereumElapse(channelId:"${channelId}") { elapse } toPolkadotElapse(channelId:"${channelId}") { elapse } }` + let result = await queryByGraphQL(query) + return result +} + +export const queryByGraphQL = async (query: string) => { + let response = await fetch(graphqlApiUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + query, + }), + }) + let data = await response.json() + return data?.data +} diff --git a/web/packages/api/src/utils.ts b/web/packages/api/src/utils.ts index fc319d9996..e78dbaae82 100644 --- a/web/packages/api/src/utils.ts +++ b/web/packages/api/src/utils.ts @@ -131,3 +131,10 @@ export const fetchEstimatedDeliveryTime = async (graphqlUrl: string, channelId: let data = await response.json() return data?.data } + +export const getEventIndex = (id: string) => { + let parts = id.split("-") + let blockNumber = parseInt(parts[0]) + let eventIndex = parseInt(parts[2]) + return `${blockNumber}-${eventIndex}` +} diff --git a/web/packages/operations/package.json b/web/packages/operations/package.json index 57f4f59778..3b336119c1 100644 --- a/web/packages/operations/package.json +++ b/web/packages/operations/package.json @@ -19,6 +19,7 @@ "smokeTest": "npx ts-node src/transfer_token.ts start", "transferToPolkadot": "npx ts-node src/transfer_to_polkadot.ts start", "transferToEthereum": "npx ts-node src/transfer_to_ethereum.ts start", + "history": "npx ts-node src/global_transfer_history.ts", "format": "prettier src --write" }, "devDependencies": { diff --git a/web/packages/operations/src/global_transfer_history.ts b/web/packages/operations/src/global_transfer_history.ts index e936b1f21f..153d2f1f1e 100644 --- a/web/packages/operations/src/global_transfer_history.ts +++ b/web/packages/operations/src/global_transfer_history.ts @@ -1,128 +1,18 @@ -import { contextFactory, destroyContext, environment, subscan, history } from "@snowbridge/api" +import "dotenv/config" +import { contextFactory, destroyContext, environment, subscan, historyV2 } from "@snowbridge/api" import { BeefyClient__factory, IGateway__factory } from "@snowbridge/contract-types" import { AlchemyProvider } from "ethers" const monitor = async () => { - const subscanKey = process.env.REACT_APP_SUBSCAN_KEY ?? "" + const toEthereum = await historyV2.toEthereumHistory() + console.log(JSON.stringify(toEthereum, null, 2)) - let env = "rococo_sepolia" - if (process.env.NODE_ENV !== undefined) { - env = process.env.NODE_ENV - } - const snowbridgeEnv = environment.SNOWBRIDGE_ENV[env] - if (snowbridgeEnv === undefined) { - throw Error(`Unknown environment '${env}'`) - } - - const { config, ethChainId } = snowbridgeEnv - if (!config.SUBSCAN_API) throw Error(`Environment ${env} does not support subscan.`) - - const ethereumProvider = new AlchemyProvider(ethChainId, process.env.REACT_APP_ALCHEMY_KEY) - const context = await contextFactory({ - ethereum: { - execution_url: ethereumProvider, - beacon_url: config.BEACON_HTTP_API, - }, - polkadot: { - url: { - bridgeHub: config.BRIDGE_HUB_URL, - assetHub: config.ASSET_HUB_URL, - relaychain: config.RELAY_CHAIN_URL, - parachains: config.PARACHAINS, - }, - }, - appContracts: { - gateway: config.GATEWAY_CONTRACT, - beefy: config.BEEFY_CONTRACT, - }, - }) - - const ethBlockTimeSeconds = 12 - const polkadotBlockTimeSeconds = 9 - const ethereumSearchPeriodBlocks = (60 * 60 * 24 * 7 * 2) / ethBlockTimeSeconds // 2 Weeks - const polkadotSearchPeriodBlocks = (60 * 60 * 24 * 7 * 2) / polkadotBlockTimeSeconds // 2 Weeks - - const assetHubScan = subscan.createApi(config.SUBSCAN_API.ASSET_HUB_URL, subscanKey) - const bridgeHubScan = subscan.createApi(config.SUBSCAN_API.BRIDGE_HUB_URL, subscanKey) - const relaychainScan = subscan.createApi(config.SUBSCAN_API.RELAY_CHAIN_URL, subscanKey) - const skipLightClientUpdates = true - - const [ - ethNowBlock, - assetHubNowBlock, - bridgeHubNowBlock, - bridgeHubParaIdCodec, - assetHubParaIdCodec, - ] = await Promise.all([ - context.ethereum.api.getBlock("latest"), - context.polkadot.api.assetHub.rpc.chain.getHeader(), - context.polkadot.api.bridgeHub.rpc.chain.getHeader(), - context.polkadot.api.bridgeHub.query.parachainInfo.parachainId(), - context.polkadot.api.assetHub.query.parachainInfo.parachainId(), - ]) - - if (ethNowBlock == null) throw Error("Cannot fetch block") - const bridgeHubParaId = bridgeHubParaIdCodec.toPrimitive() as number - const assetHubParaId = assetHubParaIdCodec.toPrimitive() as number - const beacon_url = context.config.ethereum.beacon_url - const beefyClient = BeefyClient__factory.connect(config.BEEFY_CONTRACT, ethereumProvider) - const gateway = IGateway__factory.connect(config.GATEWAY_CONTRACT, ethereumProvider) - - const [toEthereum, toPolkadot] = [ - await history.toEthereumHistory( - assetHubScan, - bridgeHubScan, - relaychainScan, - { - assetHub: { - fromBlock: assetHubNowBlock.number.toNumber() - polkadotSearchPeriodBlocks, - toBlock: assetHubNowBlock.number.toNumber(), - }, - bridgeHub: { - fromBlock: bridgeHubNowBlock.number.toNumber() - polkadotSearchPeriodBlocks, - toBlock: bridgeHubNowBlock.number.toNumber(), - }, - ethereum: { - fromBlock: ethNowBlock.number - ethereumSearchPeriodBlocks, - toBlock: ethNowBlock.number, - }, - }, - skipLightClientUpdates, - ethChainId, - assetHubParaId, - beefyClient, - gateway - ), - await history.toPolkadotHistory( - assetHubScan, - bridgeHubScan, - { - assetHub: { - fromBlock: assetHubNowBlock.number.toNumber() - polkadotSearchPeriodBlocks, - toBlock: assetHubNowBlock.number.toNumber(), - }, - bridgeHub: { - fromBlock: bridgeHubNowBlock.number.toNumber() - polkadotSearchPeriodBlocks, - toBlock: bridgeHubNowBlock.number.toNumber(), - }, - ethereum: { - fromBlock: ethNowBlock.number - ethereumSearchPeriodBlocks, - toBlock: ethNowBlock.number, - }, - }, - skipLightClientUpdates, - bridgeHubParaId, - gateway, - ethereumProvider, - beacon_url - ), - ] + const toPolkadot = await historyV2.toPolkadotHistory() + console.log(JSON.stringify(toPolkadot, null, 2)) const transfers = [...toEthereum, ...toPolkadot] transfers.sort((a, b) => b.info.when.getTime() - a.info.when.getTime()) console.log(JSON.stringify(transfers, null, 2)) - - await destroyContext(context) } monitor()