Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"node-fetch": "^2.6.0",
"prom-client": "^15.1.0",
"serverlet": "^0.1.1",
"viem": "^2.27.0",
"ws": "^8.18.0",
"yaob": "^0.3.12",
"yavent": "^0.1.4"
Expand All @@ -65,7 +66,7 @@
"rimraf": "^3.0.0",
"sucrase": "^3.16.0",
"ts-jest": "^29.2.6",
"typescript": "^4.6.2",
"typescript": "^5.8.3",
"wait-for-expect": "^3.0.2"
}
}
3 changes: 2 additions & 1 deletion pm2.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
{
"name": "changeServer",
"script": "./lib/index.js",
"out_file": "/var/log/pm2/changeServer.log"
"out_file": "/var/log/pm2/changeServer.log",
"error_file": "/var/log/pm2/changeServer.err.log"
}
]
}
89 changes: 79 additions & 10 deletions src/plugins/allPlugins.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { serverConfig } from '../serverConfig'
import { AddressPlugin } from '../types/addressPlugin'
import { BlockbookOptions, makeBlockbook } from './blockbook'
import { makeEvmRpc } from './evmRpc'
import { makeFakePlugin } from './fakePlugin'

function makeNowNode(opts: BlockbookOptions): AddressPlugin {
Expand Down Expand Up @@ -35,25 +36,93 @@ export const allPlugins = [
}),

// Ethereum family:
makeNowNode({
makeEvmRpc({
pluginId: 'abstract',
url: 'https://api.mainnet.abs.xyz'
}),
makeEvmRpc({
pluginId: 'amoy',
url: 'https://polygon-amoy-bor-rpc.publicnode.com'
}),
makeEvmRpc({
pluginId: 'arbitrum',
url: 'wss://arb-blockbook.nownodes.io/wss'
url: 'https://arbitrum-one-rpc.publicnode.com'
}),
makeNowNode({
makeEvmRpc({
pluginId: 'avalanche',
url: 'wss://avax-blockbook.nownodes.io/wss'
url: 'https://avalanche-c-chain-rpc.publicnode.com'
}),
makeNowNode({
makeEvmRpc({
pluginId: 'base',
url: 'wss://base-blockbook.nownodes.io/wss'
url: 'https://base-rpc.publicnode.com'
}),
makeNowNode({
makeEvmRpc({
pluginId: 'binancesmartchain',
url: 'https://bsc-rpc.publicnode.com'
}),
makeEvmRpc({
pluginId: 'bobevm',
url: 'https://bob.drpc.org'
}),
makeEvmRpc({
pluginId: 'celo',
url: 'https://celo-rpc.publicnode.com'
}),
makeEvmRpc({
pluginId: 'ethereum',
url: 'wss://eth-blockbook.nownodes.io/wss'
url: 'https://ethereum-rpc.publicnode.com'
}),
makeNowNode({
makeEvmRpc({
pluginId: 'ethereumclassic',
url: 'https://geth-at.etc-network.info'
}),
makeEvmRpc({
pluginId: 'ethereumpow',
url: 'https://mainnet.ethereumpow.org'
}),
makeEvmRpc({
pluginId: 'fantom',
url: 'https://rpc.fantom.network'
}),
makeEvmRpc({
pluginId: 'filecoinfevm',
url: 'https://rpc.ankr.com/filecoin'
}),
makeEvmRpc({
pluginId: 'filecoinfevmcalibration',
url: 'https://rpc.ankr.com/filecoin_testnet'
}),
makeEvmRpc({
pluginId: 'holesky',
url: 'https://ethereum-holesky-rpc.publicnode.com'
}),
makeEvmRpc({
pluginId: 'optimism',
url: 'https://optimism-rpc.publicnode.com'
}),
makeEvmRpc({
pluginId: 'polygon',
url: 'wss://maticbook.nownodes.io/wss'
url: 'https://polygon-bor-rpc.publicnode.com'
}),
makeEvmRpc({
pluginId: 'pulsechain',
url: 'https://pulsechain-rpc.publicnode.com'
}),
makeEvmRpc({
pluginId: 'rsk',
url: 'https://public-node.rsk.co'
}),
makeEvmRpc({
pluginId: 'sepolia',
url: 'https://ethereum-sepolia-rpc.publicnode.com'
}),
makeEvmRpc({
pluginId: 'sonic',
url: 'https://sonic.drpc.org'
}),
makeEvmRpc({
pluginId: 'zksync',
url: 'https://1rpc.io/zksync2-era'
}),

// Testing:
Expand Down
129 changes: 129 additions & 0 deletions src/plugins/evmRpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { createPublicClient, http, parseAbiItem } from 'viem'
import { mainnet } from 'viem/chains'
import { makeEvents } from 'yavent'

import { AddressPlugin, PluginEvents } from '../types/addressPlugin'

export interface EvmRpcOptions {
pluginId: string

/** A clean URL for logging */
safeUrl?: string

/** The actual wss connection URL */
url: string
}

const ERC20_TRANSFER_EVENT = parseAbiItem(
'event Transfer(address indexed from, address indexed to, uint256 value)'
)

export function makeEvmRpc(opts: EvmRpcOptions): AddressPlugin {
const { pluginId, safeUrl = opts.url, url } = opts

const [on, emit] = makeEvents<PluginEvents>()

const logPrefix = `${pluginId} (${safeUrl}):`
const logger = {
log: (...args: unknown[]): void => {
console.log(logPrefix, ...args)
},
error: (...args: unknown[]): void => {
console.error(logPrefix, ...args)
},
warn: (...args: unknown[]): void => {
console.warn(logPrefix, ...args)
}
}

// Track subscribed addresses (normalized lowercase address -> original address)
const subscribedAddresses = new Map<string, string>()

const client = createPublicClient({
chain: mainnet,
transport: http(url)
})

client.watchBlocks({
includeTransactions: true,
emitMissed: true,
onError: error => {
logger.error('watchBlocks error', error)
},
onBlock: async block => {
logger.log('onBlock', block.number)
// Skip processing if no subscriptions
if (subscribedAddresses.size === 0) return

// Track which subscribed addresses have updates in this block
const addressesToUpdate = new Set<string>()

// Check regular transactions
block.transactions.forEach(tx => {
const normalizedFromAddress = tx.from.toLowerCase()
const normalizedToAddress = tx.to?.toLowerCase()
const matchingFromAddress = subscribedAddresses.get(
normalizedFromAddress
)
const matchingToAddress =
normalizedToAddress !== undefined
? subscribedAddresses.get(normalizedToAddress)
: undefined
if (matchingFromAddress != null) {
addressesToUpdate.add(matchingFromAddress)
}
if (matchingToAddress != null) {
addressesToUpdate.add(matchingToAddress)
}
})

// Check ERC20 transfers
const transferLogs = await client.getLogs({
blockHash: block.hash,
event: ERC20_TRANSFER_EVENT
})
transferLogs.forEach(log => {
const normalizedFromAddress = log.args.from?.toLowerCase()
const normalizedToAddress = log.args.to?.toLowerCase()
const matchingFromAddress =
normalizedFromAddress !== undefined
? subscribedAddresses.get(normalizedFromAddress)
: undefined
const matchingToAddress =
normalizedToAddress !== undefined
? subscribedAddresses.get(normalizedToAddress)
: undefined
if (matchingFromAddress != null) {
addressesToUpdate.add(matchingFromAddress)
}
if (matchingToAddress != null) {
addressesToUpdate.add(matchingToAddress)
}
})

// Emit update events for all affected subscribed addresses
for (const originalAddress of addressesToUpdate) {
emit('update', {
address: originalAddress,
checkpoint: block.number.toString()
})
}
}
})

const plugin: AddressPlugin = {
pluginId,
subscribe: async address => {
const normalizedAddress = address.toLowerCase()
subscribedAddresses.set(normalizedAddress, address)
return true
},
unsubscribe: async address => {
const normalizedAddress = address.toLowerCase()
return subscribedAddresses.delete(normalizedAddress)
},
on
}

return plugin
}
Loading
Loading