Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add XCM transfers support #250

Merged
merged 3 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ module.exports = {
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
"rules": {
"@typescript-eslint/camelcase": ["error", { "properties": "never" } ]
},
rules: {},
env: {
node: true,
},
Expand Down
4 changes: 2 additions & 2 deletions charts/polkadot-watcher-transaction/Chart.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
description: Polkadot Watcher
name: polkadot-watcher-transaction
version: v1.3.6
appVersion: v1.3.6
version: v1.4.0
appVersion: v1.4.0
apiVersion: v2
14 changes: 8 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "polkadot-watcher-transaction",
"version": "1.3.6",
"version": "1.4.0",
"description": "Monitor events on Polkadot networks, specifically transactions",
"repository": "git@github.com:w3f/polkadot-watcher-csv-exporter.git",
"author": "W3F Infrastructure Team <devops@web3.foundation>",
Expand All @@ -15,12 +15,14 @@
"build": "tsc --build tsconfig.json",
"prepare": "yarn build",
"pretest": "yarn lint",
"test": "mocha --timeout 60000 --require ts-node/register --exit test/*.ts test/**/*.ts",
"e2e-test": "mocha --timeout 300000 --require ts-node/register --exit e2e-test/**/*.ts",
"test": "mocha --timeout 60000 --require ts-node/register --exit test/*.ts",
"test:integration": "mocha --timeout 60000 --require ts-node/register --exit test/integration/*.ts",
"start": "node ./dist/index.js start"
},
"dependencies": {
"@polkadot/api": "^12.0.1",
"@polkadot/api": "12.0.1",
ironoa marked this conversation as resolved.
Show resolved Hide resolved
"@polkadot/types": "12.0.1",
"@polkadot/typegen": "12.0.1",
"@w3f/config": "^0.1.1",
"@w3f/logger": "^0.4.2",
"commander": "^4.0.0",
Expand All @@ -39,8 +41,8 @@
"@types/node": "14.18.20",
"@types/sinon": "10.0.11",
"@types/tmp": "0.2.3",
"@typescript-eslint/eslint-plugin": "2.34.0",
"@typescript-eslint/parser": "2.34.0",
"@typescript-eslint/eslint-plugin": "^5.26.0",
"@typescript-eslint/parser": "^5.26.0",
"@w3f/polkadot-api-client": "^1.3.0",
"@w3f/test-utils": "^1.4.0",
"chai": "4.3.6",
Expand Down
70 changes: 70 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,73 @@ export const retriesBeforeLeave = 5
export const delayBeforeRetryMillis = 5000 //5 seconds
export const dataFileName = "lastChecked.txt"
export const environment = "production"
export const parachainNames = {
polkadot: {
"1000": "AssetHub Polkadot",
"1001": "Collectives Polkadot",
"1002": "BridgeHub Polkadot",
"1004": "People Polkadot",
"2000": "Acala",
"2002": "Clover",
"2004": "Moonbeam",
"2006": "Astar",
"2008": "Crust",
"2012": "Parallel",
"2013": "Litentry",
"2025": "SORA",
"2026": "Nodle",
"2030": "Bifrost",
"2031": "Centrifuge",
"2032": "Interlay",
"2034": "Hydration",
"2035": "Phala Network",
"2037": "Unique Network",
"2040": "Polkadex",
"2043": "NeuroWeb",
"2046": "Darwinia2",
"2051": "Ajuna",
"2056": "Aventus",
"2086": "KILT Protocol",
"2090": "OAK Network",
"2092": "Zeitgeist",
"2093": "Hashed Network",
"2094": "Pendulum",
"2104": "Manta",
"3338": "peaq",
"3345": "Energy Web X",
"3346": "Continuum",
"3369": "Mythos"
},
kusama: {
"1000": "AssetHub Kusama",
"1001": "Collectives Kusama",
"1002": "BridgeHub Kusama",
"1004": "People Kusama",
"1005": "Coretime Kusama",
"2000": "Karura",
"2001": "Bifrost Kusama",
"2004": "Khala Network",
"2007": "Shiden",
"2011": "SORA",
"2012": "Crust Shadow",
"2023": "Moonriver",
"2024": "Genshiro",
"2048": "Robonomics",
"2084": "Calamari",
"2087": "Picasso",
"2090": "Basilisk",
"2092": "Kintsugi",
"2095": "Quartz",
"2096": "Pioneer",
"2105": "Crab2",
"2106": "Litmus",
"2110": "Mangata",
"2113": "Kabocha",
"2114": "Turing Network",
"2119": "Bajun Network",
"2239": "Acurast",
"2241": "krest",
"3339": "Curio",
"3344": "Xode"
}
};
12 changes: 4 additions & 8 deletions src/subscriber.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { ApiPromise, WsProvider } from '@polkadot/api';
import { Logger, LoggerSingleton } from './logger';
import { Text } from '@polkadot/types/primitive';
import {
InputConfig, PromClient, SubscriberConfig, TransactionData, TransactionType
} from './types';
import { InputConfig, PromClient, SubscriberConfig, TransactionData, TransactionType } from './types';
import { EventScannerBased } from './subscriptionModules/eventScannerBased';
import { SubscriptionModuleConstructorParams } from './subscriptionModules/ISubscribscriptionModule';
import { Notifier } from './notifier/INotifier';
import { BalanceBelowThreshold } from './subscriptionModules/balanceBelowThreshold';


export class Subscriber {
private chain: Text;
private api: ApiPromise;
private networkId: string;
private endpoint: string;
Expand Down Expand Up @@ -86,14 +82,14 @@ export class Subscriber {
}
await this.api.isReadyOrError;

this.chain = await this.api.rpc.system.chain();
this.networkId = this.chain.toString().toLowerCase()
const chain = await this.api.rpc.system.chain();
this.networkId = chain.toString().toLowerCase()
const [nodeName, nodeVersion] = await Promise.all([
this.api.rpc.system.name(),
this.api.rpc.system.version()
]);
this.logger.info(
`You are connected to chain ${this.chain} using ${nodeName} v${nodeVersion}`
`You are connected to chain ${this.networkId} using ${nodeName} v${nodeVersion}`
);
}

Expand Down
2 changes: 0 additions & 2 deletions src/subscriptionModules/ISubscribscriptionModule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* eslint-disable @typescript-eslint/interface-name-prefix */

import { ApiPromise } from "@polkadot/api";
import { Notifier } from "../notifier/INotifier";
import { SubscriberConfig } from "../types";
Expand Down
2 changes: 1 addition & 1 deletion src/subscriptionModules/balanceBelowThreshold.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export class BalanceBelowThreshold implements ISubscriptionModule{
this.promClient.updateDesiredBalance(this.networkId,account.name,account.address,account.threshold)
}

await this.api.query.system.account.multi(this.subscriptions.map(a => a.address), (balances) => {
await this.api.query.system.account.multi(this.subscriptions.map(a => a.address), (balances: any) => {
this.subscriptions.forEach((account, index) => {
const free = balances[index].data.free;
let balance = this.formatBalance(free.toBigInt());
Expand Down
89 changes: 55 additions & 34 deletions src/subscriptionModules/eventScannerBased.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import "@polkadot/api-augment/polkadot"
import { ApiPromise} from '@polkadot/api';
import { Logger, LoggerSingleton } from '../logger';
import readline from 'readline';
import {
TransactionData, TransactionType, SubscriberConfig, Subscribable, PromClient
TransactionData, TransactionType, SubscriberConfig, Subscribable, PromClient,
ChainId, TransferInfo, ChainInfo, Event
} from '../types';
import { Event, CodecHash } from '@polkadot/types/interfaces';
import { closeFile, delay, extractTransferInfoFromEvent, getFileNames, getSubscriptionNotificationConfig, initReadFileStream, initWriteFileStream, isBalanceTransferEvent, isDirEmpty, isDirExistent, makeDir, setIntervalFunction } from '../utils';
import { formatBalance } from '@polkadot/util/format/formatBalance'
import {
closeFile, delay, getFileNames, getSubscriptionNotificationConfig, initReadFileStream,
initWriteFileStream, isDirEmpty, isDirExistent, makeDir, setIntervalFunction } from '../utils';
import { extractTransferInfoFromEvent, isTransferEvent } from '../transfers';
import { ISubscriptionModule, SubscriptionModuleConstructorParams } from './ISubscribscriptionModule';
import { Notifier } from '../notifier/INotifier';
import { dataFileName, delayBeforeRetryMillis, retriesBeforeLeave, scanIntervalMillis } from '../constants';
import { EventRecord } from "@polkadot/types/interfaces";

export class EventScannerBased implements ISubscriptionModule{

private subscriptions = new Map<string,Subscribable>()
private readonly api: ApiPromise
private readonly networkId: string
private readonly notifier: Notifier
private readonly config: SubscriberConfig
private readonly logger: Logger = LoggerSingleton.getInstance()
Expand All @@ -24,20 +27,24 @@ export class EventScannerBased implements ISubscriptionModule{
private dataFileName = dataFileName
private retriesBeforeLeave: number
private delayBeforeRetryMillis: number
private chainInfo: ChainInfo
private extractTransferInfoFromEvent: (event: Event, chainInfo: ChainInfo, blockNumber: number) => TransferInfo
private isTransferEvent: (event: Event) => boolean

private isScanOngoing = false //lock for concurrency
private isNewScanRequired = false

constructor(params: SubscriptionModuleConstructorParams, private readonly promClient: PromClient) {
this.api = params.api
this.networkId = params.networkId
this.notifier = params.notifier
this.config = params.config
this.dataDir = this.config.modules.transferEventScanner.dataDir
this.scanIntervalMillis = this.config.modules.transferEventScanner.scanIntervalMillis ? this.config.modules.transferEventScanner.scanIntervalMillis : scanIntervalMillis
this.delayBeforeRetryMillis = this.config.modules.transferEventScanner.delayBeforeRetryMillis ? this.config.modules.transferEventScanner.delayBeforeRetryMillis : delayBeforeRetryMillis
this.retriesBeforeLeave = this.config.modules.transferEventScanner.retriesBeforeLeave ? this.config.modules.transferEventScanner.retriesBeforeLeave : retriesBeforeLeave

this.extractTransferInfoFromEvent = extractTransferInfoFromEvent
this.isTransferEvent = isTransferEvent
this.initChainInfo()
this._initSubscriptions()
}

Expand All @@ -47,10 +54,21 @@ export class EventScannerBased implements ISubscriptionModule{
}
}

private async initChainInfo() {
this.api.rpc.system.chain().then((chain) => {
this.chainInfo = {
id: chain.toString().toLowerCase() as ChainId,
decimals: this.api.registry.chainDecimals,
tokens: this.api.registry.chainTokens,
SS58: this.api.registry.chainSS58
}
})
}

public subscribe = async (): Promise<void> => {

await this._initDataDir()
this.promClient.updateScanHeight(this.networkId,await this._getLastCheckedBlock())//init prometheus metric
this.promClient.updateScanHeight(this.chainInfo.id,await this._getLastCheckedBlock())//init prometheus metric

await this._handleEventsSubscriptions() // scan immediately after a event detection
this.logger.info(`Event Scanner Based Module subscribed...`)
Expand All @@ -74,19 +92,23 @@ export class EventScannerBased implements ISubscriptionModule{
}

private _handleEventsSubscriptions = async (): Promise<void> => {
this.api.query.system.events((events) => {
events.forEach(async (record) => {
const { event } = record;
if(isBalanceTransferEvent(event,this.api)) await this._handleBalanceTransferEvents(event)
})
this.api.query.system.events(async (records: EventRecord[]) => {
for (const { event } of records) {
if (this.isTransferEvent(event)) {
try {
const currentBlockNumber = (await this.api.rpc.chain.getHeader()).number.unwrap().toNumber()
const { from, to } = this.extractTransferInfoFromEvent(event, this.chainInfo, currentBlockNumber)
if(this.subscriptions.has(from) || this.subscriptions.has(to)) this._requestNewScan()
} catch (error) {
this.logger.error(`TransferInfo extraction failed: ${error}`)
this.logger.warn('quitting...')
process.exit(-1);
}
}
}
})
}

private _handleBalanceTransferEvents = async (event: Event): Promise<void> => {
const {from,to} = extractTransferInfoFromEvent(event)
if(this.subscriptions.has(from) || this.subscriptions.has(to)) this._requestNewScan()
}

private _requestNewScan = async (): Promise<void> => {
if(this.isScanOngoing){
/*
Expand Down Expand Up @@ -132,20 +154,21 @@ export class EventScannerBased implements ISubscriptionModule{
const blockHash = await this.api.rpc.chain.getBlockHash(blockNumber)
const block = await this.api.rpc.chain.getBlock(blockHash)
const allRecords = await this.api.query.system.events.at(blockHash);



for (const [index, { hash }] of block.block.extrinsics.entries()) {
for (const {event} of allRecords
.filter(({ phase,event }) =>
phase.isApplyExtrinsic &&
phase.asApplyExtrinsic.eq(index) &&
isBalanceTransferEvent(event,this.api)
isTransferEvent(event)
)) {

let retriesBeforeLeave = this.retriesBeforeLeave
do {
result = await this._balanceTransferHandler(event, hash)
const { from, to, amount } = this.extractTransferInfoFromEvent(event, this.chainInfo, blockNumber)
result = await this._transferNotificationHandler(from, to, amount, hash.toString())
if(!result){
retriesBeforeLeave--
this.logger.warn(`New retry at block ${blockNumber} !!`)
Expand All @@ -170,10 +193,8 @@ export class EventScannerBased implements ISubscriptionModule{
this.logger.info(`\n*****\nSCAN completed at block ${await this._getLastCheckedBlock()}\n*****`)
}

private _balanceTransferHandler = async (event: Event, extrinsicHash: CodecHash): Promise<boolean> => {
private _transferNotificationHandler = async (from: string, to: string, amount: string, extrinsicHash: string): Promise<boolean> => {
//this.logger.debug('Balances Transfer Event Detected')
const {from,to,amount} = extractTransferInfoFromEvent(event)

let isNewNotificationDelivered = false
let isNewNotificationNecessary = false

Expand All @@ -185,15 +206,15 @@ export class EventScannerBased implements ISubscriptionModule{
const data: TransactionData = {
name: this.subscriptions.get(from).name,
address: from,
networkId: this.networkId,
networkId: this.chainInfo.id,
txType: TransactionType.Sent,
hash: extrinsicHash.toString(),
amount: formatBalance(amount,{decimals:this.api.registry.chainDecimals[0]})
hash: extrinsicHash,
amount
};

notificationConfigFrom = getSubscriptionNotificationConfig(this.config.modules?.transferEventScanner,this.subscriptions.get(from).transferEventScanner)
if(notificationConfigFrom.sent){
this.logger.info(`Balances Transfer Event from ${from} detected`)
this.logger.info(`Transfer from ${from} detected`)
isNewNotificationDelivered = await this._notifyNewTransfer(data)
}
}
Expand All @@ -203,22 +224,22 @@ export class EventScannerBased implements ISubscriptionModule{
const data: TransactionData = {
name: this.subscriptions.get(to).name,
address: to,
networkId: this.networkId,
networkId: this.chainInfo.id,
txType: TransactionType.Received,
hash: extrinsicHash.toString(),
amount: formatBalance(amount,{decimals:this.api.registry.chainDecimals[0]})
hash: extrinsicHash,
amount
};

notificationConfigTo = getSubscriptionNotificationConfig(this.config.modules?.transferEventScanner,this.subscriptions.get(to).transferEventScanner)
if(notificationConfigTo.received){
this.logger.info(`Balances Transfer Event to ${to} detected`)
this.logger.info(`Transfer to ${to} detected`)
isNewNotificationDelivered = await this._notifyNewTransfer(data)
}
}

if(isNewNotificationNecessary && !notificationConfigFrom?.sent && !notificationConfigTo?.received){
isNewNotificationNecessary = false
this.logger.debug(`Balances Transfer Event from ${from} to ${to} detected. Notification SUPPRESSED`)
this.logger.debug(`Transfer from ${from} to ${to} detected. Notification SUPPRESSED`)
}

return isNewNotificationDelivered || !isNewNotificationNecessary
Expand Down Expand Up @@ -252,7 +273,7 @@ export class EventScannerBased implements ISubscriptionModule{
const file = initWriteFileStream(this.dataDir,this.dataFileName,this.logger)
const result = file.write(blockNumber.toString())
await closeFile(file)
if(result) this.promClient.updateScanHeight(this.networkId,blockNumber)
if(result) this.promClient.updateScanHeight(this.chainInfo.id,blockNumber)
return result
}

Expand Down
Loading
Loading