From 20c271c6c7367858ae37ecf1779cfafb3e18e76f Mon Sep 17 00:00:00 2001 From: Alistair Singh Date: Wed, 29 May 2024 04:03:30 +0200 Subject: [PATCH] Snowbridge/API version 0.1.6 (#1211) * improve start up performance * history to ethereum working * fmt * toPolkadot tracking working * fmt * run all * remove cache * final fixes * added max consumers check * more fixes * remove logging * check max consumers * update version * remove apis not compatible with next * decode transaction detail * fmt * tracking id * added when field * fix beneficiary address * source addresses * one fetch at a time * add asset type * fmt --- web/packages/api/package.json | 2 +- web/packages/api/src/assets.ts | 11 +- web/packages/api/src/environment.ts | 21 + web/packages/api/src/history.ts | 1062 +++++++++++++++++ web/packages/api/src/index.ts | 63 +- web/packages/api/src/subscan.ts | 204 ++++ web/packages/api/src/toEthereum.ts | 16 +- web/packages/api/src/toPolkadot.ts | 60 +- web/packages/contract-types/package.json | 2 +- .../operations/src/global_transfer_history.ts | 99 ++ web/packages/operations/src/transfer_token.ts | 8 +- web/pnpm-lock.yaml | 13 +- 12 files changed, 1512 insertions(+), 49 deletions(-) create mode 100644 web/packages/api/src/history.ts create mode 100644 web/packages/api/src/subscan.ts create mode 100644 web/packages/operations/src/global_transfer_history.ts diff --git a/web/packages/api/package.json b/web/packages/api/package.json index 82e8ac1d70..9b83bf1fd2 100644 --- a/web/packages/api/package.json +++ b/web/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@snowbridge/api", - "version": "0.1.5", + "version": "0.1.6", "description": "Snowbridge API client", "license": "Apache-2.0", "repository": { diff --git a/web/packages/api/src/assets.ts b/web/packages/api/src/assets.ts index b05d175820..0ba35f71af 100644 --- a/web/packages/api/src/assets.ts +++ b/web/packages/api/src/assets.ts @@ -106,7 +106,16 @@ export const assetErc20Balance = async ( } } -export const assetErc20Metadata = async (context: Context, tokenAddress: string) => { +export type ERC20Metadata = { + name: string + symbol: string + decimals: bigint +} + +export const assetErc20Metadata = async ( + context: Context, + tokenAddress: string +): Promise => { const tokenMetadata = IERC20Metadata__factory.connect(tokenAddress, context.ethereum.api) const [name, symbol, decimals] = await Promise.all([ tokenMetadata.name(), diff --git a/web/packages/api/src/environment.ts b/web/packages/api/src/environment.ts index 8a1980bd4e..4f3ea4c5d9 100644 --- a/web/packages/api/src/environment.ts +++ b/web/packages/api/src/environment.ts @@ -12,6 +12,11 @@ export type Config = { SECONDARY_GOVERNANCE_CHANNEL_ID: string RELAYERS: Relayer[] PARACHAINS: string[] + SUBSCAN_API?: { + RELAY_CHAIN_URL: string + ASSET_HUB_URL: string + BRIDGE_HUB_URL: string + } } export type SourceType = "substrate" | "ethereum" @@ -21,6 +26,7 @@ export type ParachainInfo = { destinationFeeDOT: bigint has20ByteAccounts: boolean decimals: number + maxConsumers: number ss58Format?: number } export type TransferLocation = { @@ -61,6 +67,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { destinationFeeDOT: 0n, has20ByteAccounts: false, decimals: 12, + maxConsumers: 16, }, erc20tokensReceivable: { WETH: "0x87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d", @@ -76,6 +83,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { destinationFeeDOT: 4_000_000_000n, has20ByteAccounts: false, decimals: 12, + maxConsumers: 16, }, erc20tokensReceivable: { WETH: "0x87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d", @@ -165,6 +173,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { destinationFeeDOT: 0n, has20ByteAccounts: false, decimals: 12, + maxConsumers: 16, }, erc20tokensReceivable: { WETH: "0xfff9976782d46cc05630d1f6ebab18b2324d6b14", @@ -182,6 +191,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { destinationFeeDOT: 200_000_000_000n, has20ByteAccounts: true, decimals: 12, + maxConsumers: 16, }, erc20tokensReceivable: { MUSE: "0xb34a6924a02100ba6ef12af1c798285e8f7a16ee", @@ -235,6 +245,11 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { type: "ethereum", }, ], + SUBSCAN_API: { + RELAY_CHAIN_URL: "https://rococo.api.subscan.io", + ASSET_HUB_URL: "https://assethub-rococo.api.subscan.io", + BRIDGE_HUB_URL: "https://bridgehub-rococo.api.subscan.io", + }, }, }, polkadot_mainnet: { @@ -259,6 +274,7 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { destinationFeeDOT: 0n, has20ByteAccounts: false, decimals: 10, + maxConsumers: 64, }, erc20tokensReceivable: { WETH: "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", @@ -312,6 +328,11 @@ export const SNOWBRIDGE_ENV: { [id: string]: SnowbridgeEnvironment } = { type: "ethereum", }, ], + SUBSCAN_API: { + RELAY_CHAIN_URL: "https://polkadot.api.subscan.io", + ASSET_HUB_URL: "https://assethub-polkadot.api.subscan.io", + BRIDGE_HUB_URL: "https://bridgehub-polkadot.api.subscan.io", + }, }, }, } diff --git a/web/packages/api/src/history.ts b/web/packages/api/src/history.ts new file mode 100644 index 0000000000..58ad55d712 --- /dev/null +++ b/web/packages/api/src/history.ts @@ -0,0 +1,1062 @@ +import { Context } from "./index" +import { fetchBeaconSlot, paraIdToChannelId } from "./utils" +import { SubscanApi, fetchEvents, fetchExtrinsics } from "./subscan" +import { forwardedTopicId } 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 ( + context: Context, + assetHubScan: SubscanApi, + bridgeHubScan: SubscanApi, + range: { + assetHub: { fromBlock: number; toBlock: number } + bridgeHub: { fromBlock: number; toBlock: number } + ethereum: { fromBlock: number; toBlock: number } + } +): Promise => { + console.log("Fetching history To Polkadot") + console.log( + `eth from ${range.ethereum.fromBlock} to ${range.ethereum.toBlock} (${ + range.ethereum.toBlock - range.ethereum.fromBlock + } blocks)` + ) + console.log( + `assethub from ${range.assetHub.fromBlock} to ${range.assetHub.toBlock} (${ + range.assetHub.toBlock - range.assetHub.fromBlock + } blocks)` + ) + console.log( + `bridgehub from ${range.bridgeHub.fromBlock} to ${range.bridgeHub.toBlock} (${ + range.bridgeHub.toBlock - range.bridgeHub.fromBlock + } blocks)` + ) + + const bridgeHubParaIdCodec = + await context.polkadot.api.bridgeHub.query.parachainInfo.parachainId() + const bridgeHubParaId = bridgeHubParaIdCodec.toPrimitive() as number + + const [ + ethOutboundMessages, + beaconClientUpdates, + inboundMessagesReceived, + assetHubMessageQueue, + ] = [ + await getEthOutboundMessages(context, range.ethereum.fromBlock, range.ethereum.toBlock), + + await getBeaconClientUpdates( + bridgeHubScan, + range.bridgeHub.fromBlock, + range.bridgeHub.toBlock + ), + + await getBridgeHubInboundMessages( + bridgeHubScan, + range.bridgeHub.fromBlock, + range.bridgeHub.toBlock + ), + + await getAssetHubMessageQueueProccessed( + assetHubScan, + bridgeHubParaId, + range.assetHub.fromBlock, + range.assetHub.toBlock + ), + ] + + console.log("number of transfers", ethOutboundMessages.length) + console.log("number of beacon client updates", beaconClientUpdates.length) + console.log("number of inbound messages received", inboundMessagesReceived.length) + console.log("number of asset hub message queue processed", assetHubMessageQueue.length) + + const results: ToPolkadotTransferResult[] = [] + for (const outboundMessage of ethOutboundMessages) { + const result: ToPolkadotTransferResult = { + id: `${outboundMessage.transactionHash}-${outboundMessage.data.messageId}`, + status: TransferStatus.Pending, + info: { + when: new Date(outboundMessage.data.timestamp * 1000), + sourceAddress: outboundMessage.data.sourceAddress, + beneficiaryAddress: outboundMessage.data.beneficiaryAddress, + tokenAddress: outboundMessage.data.tokenAddress, + destinationParachain: outboundMessage.data.destinationParachain, + destinationFee: outboundMessage.data.destinationFee, + amount: outboundMessage.data.amount, + }, + submitted: { + blockHash: outboundMessage.blockHash, + blockNumber: outboundMessage.blockNumber, + logIndex: outboundMessage.logIndex, + transactionHash: outboundMessage.transactionHash, + transactionIndex: outboundMessage.transactionIndex, + channelId: outboundMessage.data.channelId, + messageId: outboundMessage.data.messageId, + nonce: outboundMessage.data.nonce, + parentBeaconSlot: Number(outboundMessage.data.parentBeaconSlot), + }, + } + results.push(result) + + const beaconClientIncluded = beaconClientUpdates.find( + (ev) => ev.data.beaconSlot > result.submitted.parentBeaconSlot + 1 // add one to parent to get current + ) + if (beaconClientIncluded) { + result.beaconClientIncluded = { + extrinsic_index: beaconClientIncluded.extrinsic_index, + extrinsic_hash: beaconClientIncluded.extrinsic_hash, + event_index: beaconClientIncluded.event_index, + block_timestamp: beaconClientIncluded.block_timestamp, + beaconSlot: beaconClientIncluded.data.beaconSlot, + beaconBlockHash: beaconClientIncluded.data.beaconBlockHash, + } + } + + const inboundMessageReceived = inboundMessagesReceived.find( + (ev) => + ev.data.messageId === result.submitted.messageId && + ev.data.channelId === result.submitted.channelId && + ev.data.nonce === result.submitted.nonce + ) + if (inboundMessageReceived) { + result.inboundMessageReceived = { + extrinsic_index: inboundMessageReceived.extrinsic_index, + extrinsic_hash: inboundMessageReceived.extrinsic_hash, + event_index: inboundMessageReceived.event_index, + block_timestamp: inboundMessageReceived.block_timestamp, + messageId: inboundMessageReceived.data.messageId, + channelId: inboundMessageReceived.data.channelId, + nonce: inboundMessageReceived.data.nonce, + } + } + + const assetHubMessageProcessed = assetHubMessageQueue.find( + (ev) => + ev.data.sibling === bridgeHubParaId && + ev.data.messageId == result.submitted.messageId + ) + if (assetHubMessageProcessed) { + result.assetHubMessageProcessed = { + extrinsic_hash: assetHubMessageProcessed.extrinsic_hash, + event_index: assetHubMessageProcessed.event_index, + block_timestamp: assetHubMessageProcessed.block_timestamp, + success: assetHubMessageProcessed.data.success, + sibling: assetHubMessageProcessed.data.sibling, + } + if (!result.assetHubMessageProcessed.success) { + result.status = TransferStatus.Failed + continue + } + + result.status = TransferStatus.Complete + } + } + return results +} + +export const toEthereumHistory = async ( + context: Context, + assetHubScan: SubscanApi, + bridgeHubScan: SubscanApi, + relaychainScan: SubscanApi, + range: { + assetHub: { fromBlock: number; toBlock: number } + bridgeHub: { fromBlock: number; toBlock: number } + ethereum: { fromBlock: number; toBlock: number } + } +): Promise => { + console.log("Fetching history To Ethereum") + console.log( + `eth from ${range.ethereum.fromBlock} to ${range.ethereum.toBlock} (${ + range.ethereum.toBlock - range.ethereum.fromBlock + } blocks)` + ) + console.log( + `assethub from ${range.assetHub.fromBlock} to ${range.assetHub.toBlock} (${ + range.assetHub.toBlock - range.assetHub.fromBlock + } blocks)` + ) + console.log( + `bridgehub from ${range.bridgeHub.fromBlock} to ${range.bridgeHub.toBlock} (${ + range.bridgeHub.toBlock - range.bridgeHub.fromBlock + } blocks)` + ) + + const [ethNetwork, assetHubParaId] = await Promise.all([ + context.ethereum.api.getNetwork(), + context.polkadot.api.assetHub.query.parachainInfo.parachainId(), + ]) + const assetHubParaIdDecoded = assetHubParaId.toPrimitive() as number + const assetHubChannelId = paraIdToChannelId(assetHubParaIdDecoded) + + const [ + allTransfers, + allMessageQueues, + allOutboundMessages, + allBeefyClientUpdates, + allInboundMessages, + ] = [ + await getAssetHubTransfers( + assetHubScan, + relaychainScan, + Number(ethNetwork.chainId), + range.assetHub.fromBlock, + range.assetHub.toBlock + ), + + await getBridgeHubMessageQueueProccessed( + bridgeHubScan, + assetHubParaIdDecoded, + assetHubChannelId, + range.bridgeHub.fromBlock, + range.bridgeHub.toBlock + ), + + await getBridgeHubOutboundMessages( + bridgeHubScan, + range.bridgeHub.fromBlock, + range.bridgeHub.toBlock + ), + + await getBeefyClientUpdates(context, range.ethereum.fromBlock, range.ethereum.toBlock), + + await getEthInboundMessagesDispatched( + context, + range.ethereum.fromBlock, + range.ethereum.toBlock + ), + ] + + console.log("number of transfers", allTransfers.length) + console.log("number of message queues", allMessageQueues.length) + console.log("number of outbound messages", allOutboundMessages.length) + console.log("number of beefy updates", allBeefyClientUpdates.length) + console.log("number of inbound messages", allInboundMessages.length) + + const results: ToEthereumTransferResult[] = [] + for (const transfer of allTransfers) { + const result: ToEthereumTransferResult = { + id: `${transfer.extrinsic_hash}-${transfer.data.messageId}`, + status: TransferStatus.Pending, + info: { + when: new Date(transfer.block_timestamp * 1000), + sourceAddress: transfer.data.account_id, + tokenAddress: transfer.data.tokenAddress, + beneficiaryAddress: transfer.data.beneficiaryAddress, + amount: transfer.data.amount, + }, + submitted: { + extrinsic_index: transfer.extrinsic_index, + extrinsic_hash: transfer.extrinsic_hash, + block_hash: transfer.data.block_hash, + account_id: transfer.data.account_id, + block_num: transfer.block_num, + block_timestamp: transfer.block_timestamp, + messageId: transfer.data.messageId, + bridgeHubMessageId: transfer.data.bridgeHubMessageId, + success: transfer.data.success, + relayChain: { + block_num: transfer.data.relayChain.block_num, + block_hash: transfer.data.relayChain.block_hash, + }, + }, + } + results.push(result) + if (!result.submitted.success) { + result.status = TransferStatus.Failed + continue + } + + const bridgeHubXcmDelivered = allMessageQueues.find( + (ev: any) => + ev.data.messageId === result.submitted.bridgeHubMessageId && + ev.data.sibling == assetHubParaIdDecoded + ) + if (bridgeHubXcmDelivered) { + result.bridgeHubXcmDelivered = { + block_timestamp: bridgeHubXcmDelivered.block_timestamp, + event_index: bridgeHubXcmDelivered.event_index, + extrinsic_hash: bridgeHubXcmDelivered.extrinsic_hash, + siblingParachain: bridgeHubXcmDelivered.data.sibling, + success: bridgeHubXcmDelivered.data.success, + } + if (!result.bridgeHubXcmDelivered.success) { + result.status = TransferStatus.Failed + continue + } + } + const bridgeHubChannelDelivered = allMessageQueues.find( + (ev: any) => + ev.extrinsic_hash === result.bridgeHubXcmDelivered?.extrinsic_hash && + ev.data.channelId === assetHubChannelId && + ev.block_timestamp === result.bridgeHubXcmDelivered?.block_timestamp + ) + if (bridgeHubChannelDelivered) { + result.bridgeHubChannelDelivered = { + block_timestamp: bridgeHubChannelDelivered.block_timestamp, + event_index: bridgeHubChannelDelivered.event_index, + extrinsic_hash: bridgeHubChannelDelivered.extrinsic_hash, + channelId: bridgeHubChannelDelivered.data.channelId, + success: bridgeHubChannelDelivered.data.success, + } + if (!result.bridgeHubChannelDelivered.success) { + result.status = TransferStatus.Failed + continue + } + } + + const bridgeHubMessageQueued = allOutboundMessages.find( + (ev: any) => + ev.data.messageId === result.submitted.messageId && + ev.event_id === "MessageQueued" /* TODO: ChannelId */ + ) + if (bridgeHubMessageQueued) { + result.bridgeHubMessageQueued = { + block_timestamp: bridgeHubMessageQueued.block_timestamp, + event_index: bridgeHubMessageQueued.event_index, + extrinsic_hash: bridgeHubMessageQueued.extrinsic_hash, + } + } + const bridgeHubMessageAccepted = allOutboundMessages.find( + (ev: any) => + ev.data.messageId === result.submitted.messageId && + ev.event_id === "MessageAccepted" /* TODO: ChannelId */ + ) + if (bridgeHubMessageAccepted) { + result.bridgeHubMessageAccepted = { + block_timestamp: bridgeHubMessageAccepted.block_timestamp, + event_index: bridgeHubMessageAccepted.event_index, + extrinsic_hash: bridgeHubMessageAccepted.extrinsic_hash, + nonce: bridgeHubMessageAccepted.data.nonce, + } + } + + const secondsTillAcceptedByRelayChain = 6 /* 6 secs per block */ * 10 /* blocks */ + const ethereumBeefyIncluded = allBeefyClientUpdates.find( + (ev) => + ev.data.blockNumber > + result.submitted.relayChain.block_num + secondsTillAcceptedByRelayChain + ) + if (ethereumBeefyIncluded) { + result.ethereumBeefyIncluded = { + blockNumber: ethereumBeefyIncluded.blockNumber, + blockHash: ethereumBeefyIncluded.blockHash, + transactionHash: ethereumBeefyIncluded.transactionHash, + transactionIndex: ethereumBeefyIncluded.transactionIndex, + logIndex: ethereumBeefyIncluded.logIndex, + relayChainblockNumber: ethereumBeefyIncluded.data.blockNumber, + mmrRoot: ethereumBeefyIncluded.data.mmrRoot, + } + } + + const ethereumMessageDispatched = allInboundMessages.find( + (ev) => + ev.data.channelId === result.bridgeHubChannelDelivered?.channelId && + ev.data.messageId === result.submitted.messageId && + ev.data.nonce === result.bridgeHubMessageAccepted?.nonce + ) + + if (ethereumMessageDispatched) { + result.ethereumMessageDispatched = { + blockNumber: ethereumMessageDispatched.blockNumber, + blockHash: ethereumMessageDispatched.blockHash, + transactionHash: ethereumMessageDispatched.transactionHash, + transactionIndex: ethereumMessageDispatched.transactionIndex, + logIndex: ethereumMessageDispatched.logIndex, + messageId: ethereumMessageDispatched.data.messageId, + channelId: ethereumMessageDispatched.data.channelId, + nonce: ethereumMessageDispatched.data.nonce, + success: ethereumMessageDispatched.data.success, + } + if (!result.ethereumMessageDispatched.success) { + result.status = TransferStatus.Failed + continue + } + + result.status = TransferStatus.Complete + } + } + return results +} + +const getAssetHubTransfers = async ( + assetHubScan: SubscanApi, + relaychainScan: SubscanApi, + ethChainId: number, + fromBlock: number, + toBlock: number +) => { + const acc = [] + const rows = 100 + let page = 0 + + let endOfPages = false + while (!endOfPages) { + const { extrinsics: transfers, endOfPages: end } = await subFetchBridgeTransfers( + assetHubScan, + relaychainScan, + ethChainId, + fromBlock, + toBlock, + page, + rows + ) + endOfPages = end + acc.push(...transfers) + page++ + } + return acc +} + +const getBridgeHubMessageQueueProccessed = async ( + bridgeHubScan: SubscanApi, + assetHubParaId: number, + assetHubChannelId: string, + fromBlock: number, + toBlock: number +) => { + const acc = [] + const rows = 100 + let page = 0 + let endOfPages = false + while (!endOfPages) { + const { events, endOfPages: end } = await subFetchMessageQueueBySiblingOrChannel( + bridgeHubScan, + assetHubParaId, + assetHubChannelId, + fromBlock, + toBlock, + page, + rows + ) + endOfPages = end + acc.push(...events) + page++ + } + return acc +} + +const getBridgeHubOutboundMessages = async ( + bridgeHubScan: SubscanApi, + fromBlock: number, + toBlock: number +) => { + const acc = [] + const rows = 100 + let page = 0 + let endOfPages = false + while (!endOfPages) { + const { events, endOfPages: end } = await subFetchOutboundMessages( + bridgeHubScan, + fromBlock, + toBlock, + page, + rows + ) + endOfPages = end + acc.push(...events) + page++ + } + return acc +} + +const getBeefyClientUpdates = async (context: Context, fromBlock: number, toBlock: number) => { + const { beefyClient } = context.ethereum.contracts + const NewMMRRoot = beefyClient.getEvent("NewMMRRoot") + const roots = await beefyClient.queryFilter(NewMMRRoot, fromBlock, toBlock) + const updates = roots.map((r) => { + return { + blockNumber: r.blockNumber, + blockHash: r.blockHash, + logIndex: r.index, + transactionIndex: r.transactionIndex, + transactionHash: r.transactionHash, + data: { + blockNumber: Number(r.args.blockNumber), + mmrRoot: r.args.mmrRoot, + }, + } + }) + updates.sort((a, b) => Number(a.data.blockNumber - b.data.blockNumber)) + return updates +} + +const getEthInboundMessagesDispatched = async ( + context: Context, + fromBlock: number, + toBlock: number +) => { + const { gateway } = context.ethereum.contracts + const InboundMessageDispatched = gateway.getEvent("InboundMessageDispatched") + const inboundMessages = await gateway.queryFilter(InboundMessageDispatched, fromBlock, toBlock) + return inboundMessages.map((im) => { + return { + blockNumber: im.blockNumber, + blockHash: im.blockHash, + logIndex: im.index, + transactionIndex: im.transactionIndex, + transactionHash: im.transactionHash, + data: { + channelId: im.args.channelID, + nonce: Number(im.args.nonce), + messageId: im.args.messageID, + success: im.args.success, + }, + } + }) +} + +const subFetchBridgeTransfers = async ( + assetHub: SubscanApi, + relaychain: SubscanApi, + ethChainId: number, + fromBlock: number, + toBlock: number, + page: number, + rows = 10 +) => { + return fetchExtrinsics( + assetHub, + "polkadotxcm", + "transfer_assets", + fromBlock, + toBlock, + page, + rows, + async (extrinsic, params) => { + const dest = params.find((p: any) => p.name == "dest") + const parents: number | null = dest.value.V3?.parents ?? dest.value.V4?.parents ?? null + const chainId: number | null = + dest.value.V3?.interior?.X1?.GlobalConsensus?.Ethereum ?? + (dest.value.V4?.interior?.X1 && dest.value.V4?.interior?.X1[0])?.GlobalConsensus + ?.Ethereum ?? + null + + if (!(parents === 2 && chainId === ethChainId)) { + return null + } + + const beneficiary = params.find((p: any) => p.name == "beneficiary")?.value + const beneficiaryParents: number | null = + beneficiary.V3?.parents ?? beneficiary.V4?.parents ?? null + const beneficiaryAddress: string | null = + beneficiary.V3?.interior?.X1?.AccountKey20?.key ?? + (beneficiary.V4?.interior?.X1 && beneficiary.V4?.interior?.X1[0])?.AccountKey20 + ?.key ?? + null + + if (!(beneficiaryParents === 0 && beneficiaryAddress !== null)) { + return null + } + + const assets = params.find((p: any) => p.name == "assets")?.value + let amount: string | null = null + let tokenParents: number | null = null + let tokenAddress: string | null = null + let tokenChainId: number | null = null + for (const asset of assets.V3 ?? assets.V4 ?? []) { + amount = asset.fun?.Fungible ?? null + if (amount === null) { + continue + } + + tokenParents = asset.id?.parents ?? asset.id?.Concrete?.parents ?? null + if (tokenParents === null) { + continue + } + + const tokenX2 = + asset.id?.interior?.X2 ?? Object.values(asset.id?.Concrete?.interior?.X2 ?? {}) + if (tokenX2 === null || tokenX2.length !== 2) { + continue + } + + tokenChainId = tokenX2[0].GlobalConsensus?.Ethereum ?? null + if (tokenChainId === null) { + continue + } + + tokenAddress = tokenX2[1].AccountKey20?.key ?? null + if (tokenAddress === null) { + continue + } + + // found first token + break + } + + if ( + !( + tokenParents === 2 && + tokenChainId === ethChainId && + tokenAddress !== null && + amount !== null + ) + ) { + return null + } + + const [ + { + json: { data: transfer }, + }, + { + json: { data: relayBlock }, + }, + ] = [ + await assetHub.post("api/scan/extrinsic", { + extrinsic_index: extrinsic.extrinsic_index, + only_extrinsic_event: true, + }), + await relaychain.post("api/scan/block", { + block_timestamp: extrinsic.block_timestamp, + only_head: true, + }), + ] + const maybeEvent = transfer.event.find( + (ev: any) => ev.module_id === "polkadotxcm" && ev.event_id === "Sent" + ) + let messageId: string | null = null + let bridgeHubMessageId: string | null = null + + if (transfer.success && maybeEvent) { + const ev = JSON.parse(maybeEvent.params) + messageId = ev.find((pa: any) => pa.name === "message_id")?.value ?? null + if (messageId) { + bridgeHubMessageId = forwardedTopicId(messageId) + } + } + + const success = + transfer.event.find( + (ev: any) => ev.module_id === "system" && ev.event_id === "ExtrinsicSuccess" + ) !== undefined + + return { + events: transfer.events, + messageId, + bridgeHubMessageId, + success, + block_hash: transfer.block_hash, + account_id: transfer.account_id, + relayChain: { block_num: relayBlock.block_num, block_hash: relayBlock.hash }, + tokenAddress, + beneficiaryAddress, + amount, + } + } + ) +} + +const subFetchMessageQueueBySiblingOrChannel = async ( + api: SubscanApi, + filterSibling: number, + filterChannelId: string, + fromBlock: number, + toBlock: number, + page: number, + rows = 10 +) => { + return fetchEvents( + api, + "messagequeue", + ["Processed", "ProcessingFailed", "OverweightEnqueued"], + fromBlock, + toBlock, + page, + rows, + async (event, params) => { + const messageId = params.find((e: any) => e.name === "id")?.value + if (!messageId) { + return null + } + + const origin = params.find((e: any) => e.name === "origin")?.value + const sibling = origin?.Sibling ?? null + const channelId = origin?.Snowbridge ?? null + + if (sibling === null && channelId !== filterChannelId) { + return null + } + if (channelId === null && sibling !== filterSibling) { + return null + } + if (channelId === null && sibling === null) { + return null + } + + let success = + event.event_id === "Processed" && + (params.find((e: any) => e.name === "success")?.value ?? false) + + return { messageId, sibling, channelId, success } + } + ) +} + +const subFetchMessageQueueBySibling = async ( + api: SubscanApi, + filterSibling: number, + fromBlock: number, + toBlock: number, + page: number, + rows = 10 +) => { + return fetchEvents( + api, + "messagequeue", + ["Processed", "ProcessingFailed", "OverweightEnqueued"], + fromBlock, + toBlock, + page, + rows, + async (event, params) => { + const messageId = params.find((e: any) => e.name === "id")?.value + if (!messageId) { + return null + } + + const origin = params.find((e: any) => e.name === "origin")?.value + const sibling = origin?.Sibling + + if (sibling !== filterSibling) { + return null + } + + let success = + event.event_id === "Processed" && + (params.find((e: any) => e.name === "success")?.value ?? false) + + return { messageId, sibling, success } + } + ) +} + +const subFetchOutboundMessages = async ( + api: SubscanApi, + fromBlock: number, + toBlock: number, + page: number, + rows = 10 +) => { + return fetchEvents( + api, + "ethereumoutboundqueue", + ["MessageAccepted", "MessageQueued"], + fromBlock, + toBlock, + page, + rows, + async (_, params) => { + const messageId = params.find((e: any) => e.name === "id")?.value + // TODO: channelId + const nonce = params.find((e: any) => e.name === "nonce")?.value ?? null + return { messageId, nonce } + } + ) +} + +const getEthOutboundMessages = async (context: Context, fromBlock: number, toBlock: number) => { + const { gateway } = context.ethereum.contracts + const OutboundMessageAccepted = gateway.getEvent("OutboundMessageAccepted") + const outboundMessages = await gateway.queryFilter(OutboundMessageAccepted, fromBlock, toBlock) + const result = [] + for (const om of outboundMessages) { + const block = await om.getBlock() + const beaconBlockRoot = await fetchBeaconSlot( + context.config.ethereum.beacon_url, + block.parentBeaconBlockRoot as any + ) + const transaction = await block.getTransaction(om.transactionHash) + const [ + tokenAddress, + destinationParachain, + [addressType, beneficiaryAddress], + destinationFee, + amount, + ] = context.ethereum.contracts.gateway.interface.decodeFunctionData( + "sendToken", + transaction.data + ) + let beneficiary = beneficiaryAddress as string + switch (addressType) { + case 0n: + { + // 4-byte index + const index = BigInt(beneficiary.substring(0, 6)) + beneficiary = index.toString() + } + break + case 2n: + { + // 20-byte address + beneficiary = beneficiary.substring(0, 42) + } + break + } + + result.push({ + blockNumber: om.blockNumber, + blockHash: om.blockHash, + logIndex: om.index, + transactionIndex: om.transactionIndex, + transactionHash: om.transactionHash, + data: { + sourceAddress: transaction.from, + timestamp: block.timestamp, + channelId: om.args.channelID, + nonce: Number(om.args.nonce), + messageId: om.args.messageID, + parentBeaconSlot: Number(beaconBlockRoot.data.message.slot), + tokenAddress: tokenAddress as string, + destinationParachain: Number(destinationParachain), + beneficiaryAddress: beneficiary, + destinationFee: destinationFee.toString() as string, + amount: amount.toString() as string, + }, + }) + } + return result +} + +const getBeaconClientUpdates = async ( + bridgeHubScan: SubscanApi, + fromBlock: number, + toBlock: number +) => { + const updates = [] + const rows = 100 + let page = 0 + let endOfPages = false + while (!endOfPages) { + const { events, endOfPages: end } = await subFetchBeaconHeaderImports( + bridgeHubScan, + fromBlock, + toBlock, + page, + rows + ) + endOfPages = end + updates.push(...events) + page++ + } + updates.sort((a, b) => Number(a.data.beaconSlot - b.data.beaconSlot)) + return updates +} + +const getBridgeHubInboundMessages = async ( + bridgeHubScan: SubscanApi, + fromBlock: number, + toBlock: number +) => { + const updates = [] + const rows = 100 + let page = 0 + let endOfPages = false + while (!endOfPages) { + const { events, endOfPages: end } = await subFetchInboundMessageReceived( + bridgeHubScan, + fromBlock, + toBlock, + page, + rows + ) + endOfPages = end + updates.push(...events) + page++ + } + return updates +} + +const getAssetHubMessageQueueProccessed = async ( + bridgeHubScan: SubscanApi, + bridgeHubParaId: number, + fromBlock: number, + toBlock: number +) => { + const acc = [] + const rows = 100 + let page = 0 + let endOfPages = false + while (!endOfPages) { + const { events, endOfPages: end } = await subFetchMessageQueueBySibling( + bridgeHubScan, + bridgeHubParaId, + fromBlock, + toBlock, + page, + rows + ) + endOfPages = end + acc.push(...events) + page++ + } + return acc +} + +const subFetchBeaconHeaderImports = async ( + api: SubscanApi, + fromBlock: number, + toBlock: number, + page: number, + rows = 10 +) => { + return fetchEvents( + api, + "ethereumbeaconclient", + ["BeaconHeaderImported"], + fromBlock, + toBlock, + page, + rows, + async (_, params) => { + const beaconBlockHash = params.find((e: any) => e.name === "block_hash")?.value + const beaconSlot = params.find((e: any) => e.name === "slot")?.value + return { beaconBlockHash, beaconSlot } + } + ) +} + +const subFetchInboundMessageReceived = async ( + api: SubscanApi, + fromBlock: number, + toBlock: number, + page: number, + rows = 10 +) => { + return fetchEvents( + api, + "ethereuminboundqueue", + ["MessageReceived"], + fromBlock, + toBlock, + page, + rows, + async (_, params) => { + const channelId = params.find((e: any) => e.name === "channel_id")?.value + const nonce = params.find((e: any) => e.name === "nonce")?.value + const messageId = params.find((e: any) => e.name === "message_id")?.value + return { channelId, nonce, messageId } + } + ) +} diff --git a/web/packages/api/src/index.ts b/web/packages/api/src/index.ts index c721e914a8..6687112246 100644 --- a/web/packages/api/src/index.ts +++ b/web/packages/api/src/index.ts @@ -54,19 +54,26 @@ class EthereumContext { } } +type Parachains = { [paraId: number]: ApiPromise } + class PolkadotContext { api: { relaychain: ApiPromise assetHub: ApiPromise bridgeHub: ApiPromise - parachains: { [paraId: number]: ApiPromise } + parachains: Parachains } - constructor(relaychain: ApiPromise, assetHub: ApiPromise, bridgeHub: ApiPromise) { + constructor( + relaychain: ApiPromise, + assetHub: ApiPromise, + bridgeHub: ApiPromise, + parachains: Parachains + ) { this.api = { relaychain: relaychain, assetHub: assetHub, bridgeHub: bridgeHub, - parachains: {}, + parachains: parachains, } } } @@ -79,15 +86,32 @@ export const contextFactory = async (config: Config): Promise => { } else { ethApi = new ethers.WebSocketProvider(config.ethereum.execution_url) } - const relaychainApi = await ApiPromise.create({ - provider: new WsProvider(config.polkadot.url.relaychain), - }) - const assetHubApi = await ApiPromise.create({ - provider: new WsProvider(config.polkadot.url.assetHub), - }) - const bridgeHubApi = await ApiPromise.create({ - provider: new WsProvider(config.polkadot.url.bridgeHub), - }) + + const parasConnect: Promise<{ paraId: number; api: ApiPromise }>[] = [] + for (const parachain of config.polkadot.url.parachains ?? []) { + parasConnect.push(addParachainConnection(parachain)) + } + + const [relaychainApi, assetHubApi, bridgeHubApi] = await Promise.all([ + ApiPromise.create({ + provider: new WsProvider(config.polkadot.url.relaychain), + }), + ApiPromise.create({ + provider: new WsProvider(config.polkadot.url.assetHub), + }), + ApiPromise.create({ + provider: new WsProvider(config.polkadot.url.bridgeHub), + }), + ]) + + const paras = await Promise.all(parasConnect) + const parachains: Parachains = {} + for (const { paraId, api } of paras) { + if (paraId in parachains) { + throw new Error(`${paraId} already added.`) + } + parachains[paraId] = api + } const gatewayAddr = config.appContracts.gateway const beefyAddr = config.appContracts.beefy @@ -100,25 +124,20 @@ export const contextFactory = async (config: Config): Promise => { } const ethCtx = new EthereumContext(ethApi, appContracts) - const polCtx = new PolkadotContext(relaychainApi, assetHubApi, bridgeHubApi) + const polCtx = new PolkadotContext(relaychainApi, assetHubApi, bridgeHubApi, parachains) const context = new Context(config, ethCtx, polCtx) - for (const parachain of config.polkadot.url.parachains ?? []) { - await addParachainConnection(context, parachain) - } + await Promise.all(parasConnect) return context } -export const addParachainConnection = async (context: Context, url: string): Promise => { +export const addParachainConnection = async (url: string) => { const api = await ApiPromise.create({ provider: new WsProvider(url), }) const paraId = (await api.query.parachainInfo.parachainId()).toPrimitive() as number - if (paraId in context.polkadot.api.parachains) { - throw new Error(`${paraId} already added.`) - } - context.polkadot.api.parachains[paraId] = api console.log(`${url} added with parachain id: ${paraId}`) + return { paraId, api } } export const destroyContext = async (context: Context): Promise => { @@ -142,3 +161,5 @@ export * as utils from "./utils" export * as status from "./status" export * as assets from "./assets" export * as environment from "./environment" +export * as subscan from "./subscan" +export * as history from "./history" diff --git a/web/packages/api/src/subscan.ts b/web/packages/api/src/subscan.ts new file mode 100644 index 0000000000..20c4afaa4e --- /dev/null +++ b/web/packages/api/src/subscan.ts @@ -0,0 +1,204 @@ +export type SubscanResult = { + status: number + statusText: string + json: any + rateLimit: SubscanRateLimit +} + +export type SubscanRateLimit = { + limit: number | null + reset: number | null + remaining: number | null + retryAfter: number | null +} + +export type SubscanApiPost = (subUrl: string, body: any) => Promise +export interface SubscanApi { + post: SubscanApiPost +} + +const sleepMs = async (ms: number) => { + await new Promise((resolve) => { + const id = setTimeout(() => { + resolve() + clearTimeout(id) + }, ms) + }) +} + +export const createApi = (baseUrl: string, apiKey: string, options = { limit: 1 }): SubscanApi => { + let url = baseUrl.trim() + if (!url.endsWith("/")) { + url += "/" + } + + const headers = new Headers() + headers.append("Content-Type", "application/json") + headers.append("x-api-key", apiKey) + + let rateLimit: SubscanRateLimit = { + limit: options.limit, + reset: 0, + remaining: options.limit, + retryAfter: 0, + } + const post: SubscanApiPost = async (subUrl: string, body: any) => { + const request: RequestInit = { + method: "POST", + headers, + body: JSON.stringify(body), + redirect: "follow", + } + + if (rateLimit.retryAfter !== null && rateLimit.retryAfter > 0) { + console.log("Being rate limited", rateLimit) + await sleepMs(rateLimit.retryAfter * 1000) + } + if (rateLimit.remaining === 0 && rateLimit.reset !== null && rateLimit.reset > 0) { + console.log("Being rate limited", rateLimit) + await sleepMs(rateLimit.reset * 1000) + } + + const response = await fetch(`${url}${subUrl}`, request) + + rateLimit.limit = Number(response.headers.get("ratelimit-limit")) + rateLimit.reset = Number(response.headers.get("ratelimit-reset")) + rateLimit.remaining = Number(response.headers.get("ratelimit-remaining")) + rateLimit.retryAfter = Number(response.headers.get("retry-after")) + + if (response.status !== 200) { + throw new Error( + `Failed to fetch from Subscan: ${response.status} ${response.statusText}` + ) + } + + const json = await response.json() + return { + status: response.status, + statusText: response.statusText, + json, + rateLimit: { ...rateLimit }, + } + } + + return { + post, + } +} + +export const fetchEvents = async ( + api: SubscanApi, + module: string, + eventIds: string[], + fromBlock: number, + toBlock: number, + page: number, + rows: number, + filterMap: (events: any, params: any) => Promise +) => { + const eventsBody = { + module, + block_range: `${fromBlock}-${toBlock}`, + event_id: eventIds.length === 1 ? eventIds[0] : undefined, + row: rows, + page, + } + + const eventResponse = await api.post("api/v2/scan/events", eventsBody) + + let endOfPages = false + if (eventResponse.json.data.events === null) { + eventResponse.json.data.events = [] + endOfPages = true + } + + const map = new Map() + eventResponse.json.data.events + .filter((e: any) => eventIds.includes(e.event_id)) + .forEach((e: any) => { + map.set(e.event_index, e) + }) + + const events = [] + + if (map.size > 0) { + const paramsBody = { event_index: Array.from(map.keys()) } + const paramsResponse = await api.post("api/scan/event/params", paramsBody) + + if (paramsResponse.json.data === null) { + paramsResponse.json.data = [] + } + + for (const { event_index, params } of paramsResponse.json.data) { + const event = map.get(event_index) + const transform = await filterMap(event, params) + if (transform === null) { + continue + } + events.push({ ...event, params, data: transform }) + } + } + return { + status: eventResponse.status, + statusText: eventResponse.statusText, + events, + endOfPages, + } +} + +export const fetchExtrinsics = async ( + api: SubscanApi, + module: string, + call: string, + fromBlock: number, + toBlock: number, + page: number, + rows: number, + filterMap: (extrinsic: any, params: any) => Promise +) => { + const extBody = { + module, + call, + block_range: `${fromBlock}-${toBlock}`, + row: rows, + page, + } + const extResponse = await api.post("api/v2/scan/extrinsics", extBody) + + let endOfPages = false + if (extResponse.json.data.extrinsics === null) { + extResponse.json.data.extrinsics = [] + endOfPages = true + } + const map = new Map() + extResponse.json.data.extrinsics.forEach((e: any) => { + map.set(e.extrinsic_index, e) + }) + + const extrinsics = [] + + if (map.size > 0) { + const paramsBody = { extrinsic_index: Array.from(map.keys()) } + const extParams = await api.post("api/scan/extrinsic/params", paramsBody) + + if (extParams.json.data === null) { + extParams.json.data = [] + } + + for (const { extrinsic_index, params } of extParams.json.data) { + const event = map.get(extrinsic_index) + const transform = await filterMap(event, params) + if (transform === null) { + continue + } + + extrinsics.push({ ...event, params, data: transform }) + } + } + return { + status: extResponse.status, + statusText: extResponse.statusText, + extrinsics, + endOfPages, + } +} diff --git a/web/packages/api/src/toEthereum.ts b/web/packages/api/src/toEthereum.ts index e2fd62baff..fb06b70560 100644 --- a/web/packages/api/src/toEthereum.ts +++ b/web/packages/api/src/toEthereum.ts @@ -72,6 +72,16 @@ export type SendValidationResult = { } } +export interface IValidateOptions { + defaultFee: bigint + acceptableLatencyInSeconds: number +} + +const ValidateOptionDefaults: IValidateOptions = { + defaultFee: 2_750_872_500_000n, + acceptableLatencyInSeconds: 28800 /* 8 Hours */, +} + export const getSendFee = async ( context: Context, options = { @@ -97,11 +107,9 @@ export const validateSend = async ( beneficiary: string, tokenAddress: string, amount: bigint, - options = { - defaultFee: 2_750_872_500_000n, - acceptableLatencyInSeconds: 28800 /* 8 Hours */, - } + validateOptions: Partial = {} ): Promise => { + const options = { ...ValidateOptionDefaults, ...validateOptions } const { ethereum, ethereum: { diff --git a/web/packages/api/src/toPolkadot.ts b/web/packages/api/src/toPolkadot.ts index b5742681b2..422aa7f1c7 100644 --- a/web/packages/api/src/toPolkadot.ts +++ b/web/packages/api/src/toPolkadot.ts @@ -3,7 +3,7 @@ import { Codec } from "@polkadot/types/types" import { u8aToHex } from "@polkadot/util" import { IERC20__factory, IGateway__factory, WETH9__factory } from "@snowbridge/contract-types" import { MultiAddressStruct } from "@snowbridge/contract-types/src/IGateway" -import { LogDescription, Signer, TransactionReceipt, ethers, keccak256 } from "ethers" +import { LogDescription, Signer, TransactionReceipt, ethers } from "ethers" import { concatMap, filter, firstValueFrom, lastValueFrom, take, takeWhile, tap } from "rxjs" import { assetStatusInfo } from "./assets" import { Context } from "./index" @@ -15,11 +15,13 @@ import { paraIdToChannelId, paraIdToSovereignAccount, } from "./utils" +import { ApiPromise } from "@polkadot/api" export enum SendValidationCode { BridgeNotOperational, ChannelNotOperational, BeneficiaryAccountMissing, + BeneficiaryHasHitMaxConsumers, ForeignAssetMissing, ERC20InvalidToken, ERC20NotRegistered, @@ -68,9 +70,20 @@ export type SendValidationResult = { tokenBalance: bigint tokenSpendAllowance: bigint lightClientLatencySeconds: number + accountConsumers: number | null } } +export interface IValidateOptions { + acceptableLatencyInSeconds: number /* 3 Hours */ + maxConsumers: number +} + +const ValidateOptionDefaults: IValidateOptions = { + acceptableLatencyInSeconds: 28800 /* 3 Hours */, + maxConsumers: 16, +} + export const approveTokenSpend = async ( context: Context, signer: Signer, @@ -105,6 +118,14 @@ export const getSendFee = async ( return await gateway.quoteSendTokenFee(tokenAddress, destinationParaId, destinationFee) } +export const getSubstrateAccount = async (parachain: ApiPromise, beneficiaryHex: string) => { + const account = (await parachain.query.system.account(beneficiaryHex)).toPrimitive() as { + data: { free: string } + consumers: number + } + return { balance: account.data.free, consumers: account.consumers } +} + export const validateSend = async ( context: Context, source: ethers.Addressable, @@ -113,14 +134,13 @@ export const validateSend = async ( destinationParaId: number, amount: bigint, destinationFee: bigint, - options = { - acceptableLatencyInSeconds: 28800 /* 3 Hours */, - } + validateOptions: Partial = {} ): Promise => { + const options = { ...ValidateOptionDefaults, ...validateOptions } const { ethereum, polkadot: { - api: { assetHub, bridgeHub, relaychain }, + api: { assetHub, bridgeHub, relaychain, parachains }, }, } = context @@ -194,9 +214,11 @@ export const validateSend = async ( let { address: beneficiaryAddress, hexAddress: beneficiaryHex } = beneficiaryMultiAddress(beneficiary) - let beneficiaryAccountExists = true + let beneficiaryAccountExists = false + let hasConsumers = false let destinationChainExists = true let hrmpChannelSetup = true + let accountConsumers: number | null = null const existentialDeposit = BigInt( assetHub.consts.balances.existentialDeposit.toPrimitive() as number ) @@ -204,10 +226,10 @@ export const validateSend = async ( if (destinationFee !== 0n) throw new Error("Asset Hub does not require a destination fee.") if (beneficiaryAddress.kind !== 1) throw new Error("Asset Hub only supports 32 byte addresses.") - const account = (await assetHub.query.system.account(beneficiaryHex)).toPrimitive() as { - data: { free: string } - } - beneficiaryAccountExists = BigInt(account.data.free) > existentialDeposit + const { balance, consumers } = await getSubstrateAccount(assetHub, beneficiaryHex) + beneficiaryAccountExists = BigInt(balance) > existentialDeposit + hasConsumers = consumers + 2 <= options.maxConsumers + accountConsumers = consumers } else { const [destinationHead, hrmpChannel] = await Promise.all([ relaychain.query.paras.heads(destinationParaId), @@ -218,6 +240,17 @@ export const validateSend = async ( ]) destinationChainExists = destinationHead.toPrimitive() !== null hrmpChannelSetup = hrmpChannel.toPrimitive() !== null + + if (destinationParaId in parachains) { + const { balance, consumers } = await getSubstrateAccount(assetHub, beneficiaryHex) + beneficiaryAccountExists = BigInt(balance) > existentialDeposit + hasConsumers = consumers + 2 <= options.maxConsumers + accountConsumers = consumers + } else { + // We cannot check this as we do not know the destination. + beneficiaryAccountExists = true + hasConsumers = true + } } if (!destinationChainExists) errors.push({ @@ -229,6 +262,11 @@ export const validateSend = async ( code: SendValidationCode.BeneficiaryAccountMissing, message: "Beneficiary does not hold existential deposit on destination.", }) + if (!hasConsumers) + errors.push({ + code: SendValidationCode.BeneficiaryHasHitMaxConsumers, + message: "Benificiary is approaching the asset consumer limit. Transfer may fail.", + }) if (!hrmpChannelSetup) errors.push({ code: SendValidationCode.NoHRMPChannelToDestination, @@ -300,6 +338,7 @@ export const validateSend = async ( tokenBalance: assetInfo.ownerBalance, tokenSpendAllowance: assetInfo.tokenGatewayAllowance, existentialDeposit: existentialDeposit, + accountConsumers: accountConsumers, }, } } @@ -381,7 +420,6 @@ export const send = async ( ]) const contract = IGateway__factory.connect(context.config.appContracts.gateway, signer) - const fees = await context.ethereum.api.getFeeData() const response = await contract.sendToken( success.token, diff --git a/web/packages/contract-types/package.json b/web/packages/contract-types/package.json index d4143f9e88..c16cf18234 100644 --- a/web/packages/contract-types/package.json +++ b/web/packages/contract-types/package.json @@ -1,6 +1,6 @@ { "name": "@snowbridge/contract-types", - "version": "0.1.5", + "version": "0.1.6", "description": "Snowbridge contract type bindings", "license": "Apache-2.0", "repository": { diff --git a/web/packages/operations/src/global_transfer_history.ts b/web/packages/operations/src/global_transfer_history.ts new file mode 100644 index 0000000000..826dc16ffd --- /dev/null +++ b/web/packages/operations/src/global_transfer_history.ts @@ -0,0 +1,99 @@ +import { contextFactory, destroyContext, environment, subscan, history } from "@snowbridge/api" + +const monitor = async () => { + const subscanKey = process.env.REACT_APP_SUBSCAN_KEY ?? "" + + let env = "rococo_sepolia" + if (process.env.NODE_ENV !== undefined) { + env = process.env.NODE_ENV + } + const snwobridgeEnv = environment.SNOWBRIDGE_ENV[env] + if (snwobridgeEnv === undefined) { + throw Error(`Unknown environment '${env}'`) + } + + const { config } = snwobridgeEnv + if (!config.SUBSCAN_API) throw Error(`Environment ${env} does not support subscan.`) + + const context = await contextFactory({ + ethereum: { + execution_url: config.ETHEREUM_WS_API(process.env.REACT_APP_ALCHEMY_KEY ?? ""), + beacon_url: config.BEACON_HTTP_API, + }, + polkadot: { + url: { + bridgeHub: config.BRIDGE_HUB_WS_URL, + assetHub: config.ASSET_HUB_WS_URL, + relaychain: config.RELAY_CHAIN_WS_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 [ethNowBlock, assetHubNowBlock, bridgeHubNowBlock] = await Promise.all([ + (async () => { + const ethNowBlock = await context.ethereum.api.getBlock("latest") + if (ethNowBlock == null) throw Error("Cannot fetch block") + return ethNowBlock + })(), + context.polkadot.api.assetHub.rpc.chain.getHeader(), + context.polkadot.api.bridgeHub.rpc.chain.getHeader(), + ]) + + const [toEthereum, toPolkadot] = [ + await history.toEthereumHistory(context, 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, + }, + }), + await history.toPolkadotHistory(context, 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, + }, + }), + ] + + 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() + .then(() => process.exit(0)) + .catch((error) => { + console.error("Error:", error) + process.exit(1) + }) diff --git a/web/packages/operations/src/transfer_token.ts b/web/packages/operations/src/transfer_token.ts index 38c84c8be8..ab9a45bea3 100644 --- a/web/packages/operations/src/transfer_token.ts +++ b/web/packages/operations/src/transfer_token.ts @@ -64,7 +64,7 @@ const monitor = async () => { amount, BigInt(0) ) - console.log("Plan:", plan) + console.log("Plan:", plan, plan.failure?.errors) let result = await toPolkadot.send(context, ETHEREUM_ACCOUNT, plan) console.log("Execute:", result) while (true) { @@ -87,7 +87,7 @@ const monitor = async () => { WETH_CONTRACT, amount ) - console.log("Plan:", plan) + console.log("Plan:", plan, plan.failure?.errors) const result = await toEthereum.send(context, POLKADOT_ACCOUNT, plan) console.log("Execute:", result) while (true) { @@ -111,7 +111,7 @@ const monitor = async () => { amount, BigInt(4_000_000_000) ) - console.log("Plan:", plan) + console.log("Plan:", plan, plan.failure?.errors) let result = await toPolkadot.send(context, ETHEREUM_ACCOUNT, plan) console.log("Execute:", result) while (true) { @@ -134,7 +134,7 @@ const monitor = async () => { WETH_CONTRACT, amount ) - console.log("Plan:", plan) + console.log("Plan:", plan, plan.failure?.errors) const result = await toEthereum.send(context, POLKADOT_ACCOUNT, plan) console.log("Execute:", result) while (true) { diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 61ed8fe116..8f4e05f2a4 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -344,9 +344,6 @@ importers: '@types/lodash': specifier: ^4.14.186 version: 4.14.187 - '@types/node': - specifier: ^18.13.0 - version: 18.16.8 '@types/secp256k1': specifier: ^4.0.3 version: 4.0.3 @@ -384,6 +381,9 @@ importers: specifier: ^3.0.5 version: 3.0.5 devDependencies: + '@types/node': + specifier: ^18.16.8 + version: 18.19.31 '@typescript-eslint/eslint-plugin': specifier: ^5.42.0 version: 5.42.0(@typescript-eslint/parser@5.42.0)(eslint@8.26.0)(typescript@5.1.6) @@ -398,7 +398,7 @@ importers: version: 8.5.0(eslint@8.26.0) ts-node: specifier: ^10.9.1 - version: 10.9.1(@types/node@18.16.8)(typescript@5.1.6) + version: 10.9.1(@types/node@18.19.31)(typescript@5.1.6) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -3036,6 +3036,7 @@ packages: /@types/node@18.16.8: resolution: {integrity: sha512-p0iAXcfWCOTCBbsExHIDFCfwsqFwBTgETJveKMT+Ci3LY9YqQCI91F5S+TB20+aRCXpcWfvx5Qr5EccnwCm2NA==} + dev: true /@types/node@18.19.31: resolution: {integrity: sha512-ArgCD39YpyyrtFKIqMDvjz79jto5fcI/SVUs2HwB+f0dAzq68yqOdyaSivLiLugSziTpNXLQrVb7RZFmdZzbhA==} @@ -7378,7 +7379,7 @@ packages: yn: 3.1.1 dev: true - /ts-node@10.9.1(@types/node@18.16.8)(typescript@5.1.6): + /ts-node@10.9.1(@types/node@18.19.31)(typescript@5.1.6): resolution: {integrity: sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==} hasBin: true peerDependencies: @@ -7397,7 +7398,7 @@ packages: '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.3 - '@types/node': 18.16.8 + '@types/node': 18.19.31 acorn: 8.8.2 acorn-walk: 8.2.0 arg: 4.1.3