[Blockchain Watcher] Separating get and poll actions (#843)
* adding more overrides via env var * simplifying default cfg files * shared evm repo * move get evm logs to new action * move get solana txs to new action * minor folder cleanup * smaller docker image * add chain to evm block repo logs
This commit is contained in:
parent
b458213b0c
commit
0e4efd6673
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"printWidth": 100
|
||||
}
|
|
@ -1,12 +1,4 @@
|
|||
# syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
|
||||
FROM node:19.6.1-slim@sha256:a1ba21bf0c92931d02a8416f0a54daad66cb36a85d2b73af9d73b044f5f57cfc as builder
|
||||
|
||||
# npm wants to clone random Git repositories - lovely.
|
||||
# RUN apk add git python make build-base
|
||||
# RUN apk update && apk add bash
|
||||
RUN apt-get update && apt-get -y install \
|
||||
git python make curl netcat
|
||||
|
||||
FROM node:18.19.0-alpine3.18@sha256:c0a5f02df6e631b75ee3037bd4389ac1f91e591c5c1e30a0007a7d0babcd4cd3 as builder
|
||||
USER 1000
|
||||
|
||||
RUN mkdir -p /home/node/app
|
||||
|
@ -14,35 +6,20 @@ RUN mkdir -p /home/node/.npm
|
|||
|
||||
WORKDIR /home/node/app
|
||||
|
||||
# Fix git ssh error
|
||||
RUN git config --global url."https://".insteadOf ssh://
|
||||
|
||||
# Node
|
||||
ENV NODE_EXTRA_CA_CERTS=/certs/cert.pem
|
||||
ENV NODE_OPTIONS=--use-openssl-ca
|
||||
# npm
|
||||
RUN if [ -e /certs/cert.pem ]; then npm config set cafile /certs/cert.pem; fi
|
||||
# git
|
||||
RUN if [ -e /certs/cert.pem ]; then git config --global http.sslCAInfo /certs/cert.pem; fi
|
||||
|
||||
COPY --chown=node:node . .
|
||||
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
RUN npm run build:ncc
|
||||
|
||||
FROM node:19.6.1-slim@sha256:a1ba21bf0c92931d02a8416f0a54daad66cb36a85d2b73af9d73b044f5f57cfc as runner
|
||||
FROM node:18.19.0-alpine3.18@sha256:c0a5f02df6e631b75ee3037bd4389ac1f91e591c5c1e30a0007a7d0babcd4cd3 as runner
|
||||
|
||||
COPY --from=builder /home/node/app/config /home/node/app/config
|
||||
COPY --from=builder /home/node/app/lib /home/node/app/lib
|
||||
|
||||
WORKDIR /home/node/app
|
||||
|
||||
COPY package.json .
|
||||
COPY package-lock.json .
|
||||
|
||||
RUN npm install --omit=dev
|
||||
|
||||
CMD [ "npm", "start" ]
|
||||
CMD [ "npm", "run", "start:ncc" ]
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
Each blockchain has three watchers,
|
||||
|
||||
- A websocket watcher for low latency
|
||||
- A querying watcher
|
||||
- And a sequence gap watcher
|
||||
|
||||
These three watchers all invoke the same handler callback.
|
||||
|
||||
The handler callback is responsible for:
|
||||
|
||||
- Providing the watchers with the necessary query filter information
|
||||
- parsing the event into a persistence object
|
||||
- invoking the persistence manager
|
||||
|
||||
The persistence manager is responsible for:
|
||||
|
||||
- Inserting records into the database in a safe manner, which takes into account that items will be seen multiple times.
|
||||
- Last write wins should be the approach taken here.
|
|
@ -3,6 +3,10 @@
|
|||
"port": "PORT",
|
||||
"logLevel": "LOG_LEVEL",
|
||||
"dryRun": "DRY_RUN_ENABLED",
|
||||
"supportedChains": {
|
||||
"__name": "SUPPORTED_CHAINS",
|
||||
"__format": "json"
|
||||
},
|
||||
"jobs": {
|
||||
"dir": "JOBS_DIR"
|
||||
},
|
||||
|
@ -10,17 +14,44 @@
|
|||
"topicArn": "SNS_TOPIC_ARN",
|
||||
"region": "SNS_REGION"
|
||||
},
|
||||
"platforms": {
|
||||
"chains": {
|
||||
"ethereum": {
|
||||
"network": "ETHEREUM_NETWORK",
|
||||
"rpcs": {
|
||||
"__name": "ETHEREUM_RPCS",
|
||||
"__format": "json"
|
||||
}
|
||||
},
|
||||
"solana": {
|
||||
"network": "SOLANA_NETWORK",
|
||||
"rpcs": {
|
||||
"__name": "SOLANA_RPCS",
|
||||
"__format": "json"
|
||||
},
|
||||
"rateLimit": {
|
||||
"period": "SOLANA_RATE_LIMIT_PERIOD",
|
||||
"limit": "SOLANA_RATE_LIMIT_LIMIT"
|
||||
}
|
||||
},
|
||||
"karura": {
|
||||
"network": "KARURA_NETWORK",
|
||||
"rpcs": {
|
||||
"__name": "KARURA_RPCS",
|
||||
"__format": "json"
|
||||
}
|
||||
},
|
||||
"fantom": {
|
||||
"network": "FANTOM_NETWORK",
|
||||
"rpcs": {
|
||||
"__name": "FANTOM_RPCS",
|
||||
"__format": "json"
|
||||
}
|
||||
},
|
||||
"acala": {
|
||||
"network": "FANTOM_NETWORK",
|
||||
"rpcs": {
|
||||
"__name": "FANTOM_RPCS",
|
||||
"__format": "json"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"jobs": {
|
||||
"dir": "metadata-repo/jobs"
|
||||
},
|
||||
"platforms": {
|
||||
"chains": {
|
||||
"solana": {
|
||||
"name": "solana",
|
||||
"network": "devnet",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"platforms": {
|
||||
"chains": {
|
||||
"solana": {
|
||||
"network": "mainnet",
|
||||
"network": "mainnet-beta",
|
||||
"rpcs": ["https://api.mainnet-beta.solana.com"]
|
||||
},
|
||||
"ethereum": {
|
|
@ -1,46 +0,0 @@
|
|||
{
|
||||
"platforms": {
|
||||
"solana": {
|
||||
"name": "solana",
|
||||
"network": "mainnet-beta",
|
||||
"chainId": 1,
|
||||
"rpcs": ["https://api.mainnet-beta.solana.com"],
|
||||
"timeout": 10000
|
||||
},
|
||||
"ethereum": {
|
||||
"name": "ethereum",
|
||||
"network": "mainnet",
|
||||
"chainId": 2,
|
||||
"rpcs": ["https://rpc.ankr.com/eth"],
|
||||
"timeout": 10000
|
||||
},
|
||||
"avalanche": {
|
||||
"name": "avalanche",
|
||||
"network": "mainnet",
|
||||
"chainId": 6,
|
||||
"rpcs": ["https://api.avax.network/ext/bc/C/rpc"],
|
||||
"timeout": 10000
|
||||
},
|
||||
"fantom": {
|
||||
"name": "fantom",
|
||||
"network": "mainnet",
|
||||
"chainId": 10,
|
||||
"rpcs": ["https://rpcapi.fantom.network"],
|
||||
"timeout": 10000
|
||||
},
|
||||
"karura": {
|
||||
"name": "karura",
|
||||
"network": "mainnet",
|
||||
"chainId": 11,
|
||||
"rpcs": ["https://eth-rpc-karura.aca-api.network"],
|
||||
"timeout": 10000
|
||||
},
|
||||
"acala": {
|
||||
"name": "acala",
|
||||
"network": "mainnet",
|
||||
"chainId": 12,
|
||||
"rpcs": ["https://eth-rpc-acala.aca-api.network"],
|
||||
"timeout": 10000
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
{}
|
|
@ -1 +0,0 @@
|
|||
{}
|
|
@ -1,19 +0,0 @@
|
|||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
moduleFileExtensions: ["js", "json", "ts"],
|
||||
setupFiles: ["<rootDir>/src/infrastructure/log.ts"],
|
||||
roots: ["test", "src"],
|
||||
testRegex: ".*\\.test\\.ts$",
|
||||
transform: {
|
||||
"^.+\\.(t|j)s$": "ts-jest",
|
||||
},
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: ["**/*.(t|j)s"],
|
||||
coveragePathIgnorePatterns: ["node_modules", "test"],
|
||||
coverageDirectory: "./coverage",
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
lines: 63.9,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -10,12 +10,11 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sns": "^3.445.0",
|
||||
"@certusone/wormhole-sdk": "^0.9.21-beta.0",
|
||||
"@certusone/wormhole-sdk": "0.10.5",
|
||||
"@types/config": "^3.3.3",
|
||||
"axios": "^1.6.0",
|
||||
"bs58": "^5.0.0",
|
||||
"config": "^3.3.9",
|
||||
"dotenv": "^16.3.1",
|
||||
"ethers": "^5",
|
||||
"mollitia": "^0.1.0",
|
||||
"prom-client": "^15.0.0",
|
||||
|
@ -27,6 +26,7 @@
|
|||
"@types/koa-router": "^7.4.4",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"@types/yargs": "^17.0.23",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"jest": "^29.7.0",
|
||||
"nock": "^13.3.8",
|
||||
"prettier": "^2.8.7",
|
||||
|
@ -1190,9 +1190,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@certusone/wormhole-sdk": {
|
||||
"version": "0.9.21-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.9.21-beta.0.tgz",
|
||||
"integrity": "sha512-44oa/bvVKEtFnIwkg+NdGfVjfyiLdguRBVe4g1EK3Y/Yd6VcPtnETwJU5z5SXJvoV2KnfpnSmK8B+tMKfQ6kbg==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.10.5.tgz",
|
||||
"integrity": "sha512-wKONuigkakoFx9HplBt2Jh5KPxc7xgtDJVrIb2/SqYWbFrdpiZrMC4H6kTZq2U4+lWtqaCa1aJ1q+3GOTNx2CQ==",
|
||||
"dependencies": {
|
||||
"@certusone/wormhole-sdk-proto-web": "0.0.6",
|
||||
"@certusone/wormhole-sdk-wasm": "^0.0.1",
|
||||
|
@ -1203,7 +1203,7 @@
|
|||
"@solana/web3.js": "^1.66.2",
|
||||
"@terra-money/terra.js": "3.1.9",
|
||||
"@xpla/xpla.js": "^0.2.1",
|
||||
"algosdk": "^1.15.0",
|
||||
"algosdk": "^2.4.0",
|
||||
"aptos": "1.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"bech32": "^2.0.0",
|
||||
|
@ -5054,6 +5054,15 @@
|
|||
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@vercel/ncc": {
|
||||
"version": "0.38.1",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz",
|
||||
"integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"ncc": "dist/ncc/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@wry/context": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.3.tgz",
|
||||
|
@ -5176,13 +5185,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/algosdk": {
|
||||
"version": "1.24.1",
|
||||
"resolved": "https://registry.npmjs.org/algosdk/-/algosdk-1.24.1.tgz",
|
||||
"integrity": "sha512-9moZxdqeJ6GdE4N6fA/GlUP4LrbLZMYcYkt141J4Ss68OfEgH9qW0wBuZ3ZOKEx/xjc5bg7mLP2Gjg7nwrkmww==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/algosdk/-/algosdk-2.7.0.tgz",
|
||||
"integrity": "sha512-sBE9lpV7bup3rZ+q2j3JQaFAE9JwZvjWKX00vPlG8e9txctXbgLL56jZhSWZndqhDI9oI+0P4NldkuQIWdrUyg==",
|
||||
"dependencies": {
|
||||
"algo-msgpack-with-bigint": "^2.1.1",
|
||||
"buffer": "^6.0.2",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"buffer": "^6.0.3",
|
||||
"hi-base32": "^0.5.1",
|
||||
"js-sha256": "^0.9.0",
|
||||
"js-sha3": "^0.8.0",
|
||||
|
@ -5192,7 +5200,7 @@
|
|||
"vlq": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-escapes": {
|
||||
|
@ -6415,17 +6423,6 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/motdotla/dotenv?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/drbg.js": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz",
|
||||
|
@ -12543,9 +12540,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@certusone/wormhole-sdk": {
|
||||
"version": "0.9.21-beta.0",
|
||||
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.9.21-beta.0.tgz",
|
||||
"integrity": "sha512-44oa/bvVKEtFnIwkg+NdGfVjfyiLdguRBVe4g1EK3Y/Yd6VcPtnETwJU5z5SXJvoV2KnfpnSmK8B+tMKfQ6kbg==",
|
||||
"version": "0.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@certusone/wormhole-sdk/-/wormhole-sdk-0.10.5.tgz",
|
||||
"integrity": "sha512-wKONuigkakoFx9HplBt2Jh5KPxc7xgtDJVrIb2/SqYWbFrdpiZrMC4H6kTZq2U4+lWtqaCa1aJ1q+3GOTNx2CQ==",
|
||||
"requires": {
|
||||
"@certusone/wormhole-sdk-proto-web": "0.0.6",
|
||||
"@certusone/wormhole-sdk-wasm": "^0.0.1",
|
||||
|
@ -12559,7 +12556,7 @@
|
|||
"@solana/web3.js": "^1.66.2",
|
||||
"@terra-money/terra.js": "3.1.9",
|
||||
"@xpla/xpla.js": "^0.2.1",
|
||||
"algosdk": "^1.15.0",
|
||||
"algosdk": "^2.4.0",
|
||||
"aptos": "1.5.0",
|
||||
"axios": "^0.24.0",
|
||||
"bech32": "^2.0.0",
|
||||
|
@ -15523,6 +15520,12 @@
|
|||
"integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==",
|
||||
"dev": true
|
||||
},
|
||||
"@vercel/ncc": {
|
||||
"version": "0.38.1",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/ncc/-/ncc-0.38.1.tgz",
|
||||
"integrity": "sha512-IBBb+iI2NLu4VQn3Vwldyi2QwaXt5+hTyh58ggAMoCGE6DJmPvwL3KPBWcJl1m9LYPChBLE980Jw+CS4Wokqxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@wry/context": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.3.tgz",
|
||||
|
@ -15620,13 +15623,12 @@
|
|||
"integrity": "sha512-F1tGh056XczEaEAqu7s+hlZUDWwOBT70Eq0lfMpBP2YguSQVyxRbprLq5rELXKQOyOaixTWYhMeMQMzP0U5FoQ=="
|
||||
},
|
||||
"algosdk": {
|
||||
"version": "1.24.1",
|
||||
"resolved": "https://registry.npmjs.org/algosdk/-/algosdk-1.24.1.tgz",
|
||||
"integrity": "sha512-9moZxdqeJ6GdE4N6fA/GlUP4LrbLZMYcYkt141J4Ss68OfEgH9qW0wBuZ3ZOKEx/xjc5bg7mLP2Gjg7nwrkmww==",
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/algosdk/-/algosdk-2.7.0.tgz",
|
||||
"integrity": "sha512-sBE9lpV7bup3rZ+q2j3JQaFAE9JwZvjWKX00vPlG8e9txctXbgLL56jZhSWZndqhDI9oI+0P4NldkuQIWdrUyg==",
|
||||
"requires": {
|
||||
"algo-msgpack-with-bigint": "^2.1.1",
|
||||
"buffer": "^6.0.2",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"buffer": "^6.0.3",
|
||||
"hi-base32": "^0.5.1",
|
||||
"js-sha256": "^0.9.0",
|
||||
"js-sha3": "^0.8.0",
|
||||
|
@ -16580,11 +16582,6 @@
|
|||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "16.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
|
||||
"integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ=="
|
||||
},
|
||||
"drbg.js": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz",
|
||||
|
|
|
@ -5,9 +5,11 @@
|
|||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "node lib/start.js",
|
||||
"start:ncc": "node lib/index.js",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --collectCoverage=true",
|
||||
"build": "tsc",
|
||||
"build:ncc": "ncc build src/start.ts -o lib",
|
||||
"dev": "ts-node src/start.ts",
|
||||
"dev:debug": "ts-node src/start.ts start --debug --watch",
|
||||
"prettier": "prettier --write ."
|
||||
|
@ -16,12 +18,11 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-sns": "^3.445.0",
|
||||
"@certusone/wormhole-sdk": "^0.9.21-beta.0",
|
||||
"@certusone/wormhole-sdk": "0.10.5",
|
||||
"@types/config": "^3.3.3",
|
||||
"axios": "^1.6.0",
|
||||
"bs58": "^5.0.0",
|
||||
"config": "^3.3.9",
|
||||
"dotenv": "^16.3.1",
|
||||
"ethers": "^5",
|
||||
"mollitia": "^0.1.0",
|
||||
"prom-client": "^15.0.0",
|
||||
|
@ -33,6 +34,7 @@
|
|||
"@types/koa-router": "^7.4.4",
|
||||
"@types/uuid": "^9.0.6",
|
||||
"@types/yargs": "^17.0.23",
|
||||
"@vercel/ncc": "^0.38.1",
|
||||
"jest": "^29.7.0",
|
||||
"nock": "^13.3.8",
|
||||
"prettier": "^2.8.7",
|
||||
|
@ -43,5 +45,40 @@
|
|||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 100
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"json",
|
||||
"ts"
|
||||
],
|
||||
"setupFiles": [
|
||||
"<rootDir>/src/infrastructure/log.ts"
|
||||
],
|
||||
"roots": [
|
||||
"test",
|
||||
"src"
|
||||
],
|
||||
"testRegex": ".*\\.test\\.ts$",
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverage": true,
|
||||
"collectCoverageFrom": [
|
||||
"**/*.(t|j)s"
|
||||
],
|
||||
"coveragePathIgnorePatterns": [
|
||||
"node_modules",
|
||||
"test"
|
||||
],
|
||||
"coverageDirectory": "./coverage",
|
||||
"coverageThreshold": {
|
||||
"global": {
|
||||
"lines": 70.85
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { EvmLog } from "../../entities";
|
||||
import { EvmBlockRepository } from "../../repositories";
|
||||
import winston from "winston";
|
||||
|
||||
export class GetEvmLogs {
|
||||
private readonly blockRepo: EvmBlockRepository;
|
||||
protected readonly logger: winston.Logger;
|
||||
|
||||
constructor(blockRepo: EvmBlockRepository) {
|
||||
this.blockRepo = blockRepo;
|
||||
this.logger = winston.child({ module: "GetEvmLogs" });
|
||||
}
|
||||
|
||||
async execute(range: Range, opts: GetEvmLogsOpts): Promise<EvmLog[]> {
|
||||
if (range.fromBlock > range.toBlock) {
|
||||
this.logger.info(
|
||||
`[exec] Invalid range [fromBlock: ${range.fromBlock} - toBlock: ${range.toBlock}]`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const logs = await this.blockRepo.getFilteredLogs(opts.chain, {
|
||||
fromBlock: range.fromBlock,
|
||||
toBlock: range.toBlock,
|
||||
addresses: opts.addresses ?? [], // Works when sending multiple addresses, but not multiple topics.
|
||||
topics: opts.topics ?? [],
|
||||
});
|
||||
|
||||
const blockNumbers = new Set(logs.map((log) => log.blockNumber));
|
||||
const blocks = await this.blockRepo.getBlocks(opts.chain, blockNumbers);
|
||||
logs.forEach((log) => {
|
||||
const block = blocks[log.blockHash];
|
||||
log.blockTime = block.timestamp;
|
||||
});
|
||||
|
||||
return logs;
|
||||
}
|
||||
}
|
||||
|
||||
type Range = {
|
||||
fromBlock: bigint;
|
||||
toBlock: bigint;
|
||||
};
|
||||
|
||||
export interface GetEvmLogsOpts {
|
||||
addresses?: string[];
|
||||
topics?: string[];
|
||||
chain: string;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import { EvmLog } from "../../entities";
|
||||
import { RunPollingJob } from "../RunPollingJob";
|
||||
import { GetEvmLogs } from "./GetEvmLogs";
|
||||
import { EvmBlockRepository, MetadataRepository, StatRepository } from "../../repositories";
|
||||
import winston from "winston";
|
||||
|
||||
|
@ -14,6 +15,7 @@ export class PollEvmLogs extends RunPollingJob {
|
|||
private readonly blockRepo: EvmBlockRepository;
|
||||
private readonly metadataRepo: MetadataRepository<PollEvmLogsMetadata>;
|
||||
private readonly statsRepository: StatRepository;
|
||||
private readonly getEvmLogs: GetEvmLogs;
|
||||
private cfg: PollEvmLogsConfig;
|
||||
|
||||
private latestBlockHeight?: bigint;
|
||||
|
@ -31,6 +33,7 @@ export class PollEvmLogs extends RunPollingJob {
|
|||
this.metadataRepo = metadataRepo;
|
||||
this.statsRepository = statsRepository;
|
||||
this.cfg = cfg;
|
||||
this.getEvmLogs = new GetEvmLogs(blockRepo);
|
||||
this.logger = winston.child({ module: "PollEvmLogs", label: this.cfg.id });
|
||||
}
|
||||
|
||||
|
@ -55,29 +58,17 @@ export class PollEvmLogs extends RunPollingJob {
|
|||
protected async get(): Promise<EvmLog[]> {
|
||||
this.report();
|
||||
|
||||
this.latestBlockHeight = await this.blockRepo.getBlockHeight(this.cfg.getCommitment());
|
||||
this.latestBlockHeight = await this.blockRepo.getBlockHeight(
|
||||
this.cfg.chain,
|
||||
this.cfg.getCommitment()
|
||||
);
|
||||
|
||||
const range = this.getBlockRange(this.latestBlockHeight);
|
||||
|
||||
if (range.fromBlock > this.latestBlockHeight) {
|
||||
this.logger.info(
|
||||
`[get] Next range is after latest block height [fromBlock: ${range.fromBlock} - latestBlock: ${this.latestBlockHeight}], waiting...`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const logs = await this.blockRepo.getFilteredLogs({
|
||||
fromBlock: range.fromBlock,
|
||||
toBlock: range.toBlock,
|
||||
addresses: this.cfg.addresses, // Works when sending multiple addresses, but not multiple topics.
|
||||
topics: [], // this.cfg.topics => will be applied by handlers
|
||||
});
|
||||
|
||||
const blockNumbers = new Set(logs.map((log) => log.blockNumber));
|
||||
const blocks = await this.blockRepo.getBlocks(blockNumbers);
|
||||
logs.forEach((log) => {
|
||||
const block = blocks[log.blockHash];
|
||||
log.blockTime = block.timestamp;
|
||||
const logs = await this.getEvmLogs.execute(range, {
|
||||
chain: this.cfg.chain,
|
||||
addresses: this.cfg.addresses,
|
||||
topics: this.cfg.topics,
|
||||
});
|
||||
|
||||
this.lastRange = range;
|
||||
|
@ -151,13 +142,13 @@ export interface PollEvmLogsConfigProps {
|
|||
addresses: string[];
|
||||
topics: string[];
|
||||
id?: string;
|
||||
chain?: string;
|
||||
chain: string;
|
||||
}
|
||||
|
||||
export class PollEvmLogsConfig {
|
||||
private props: PollEvmLogsConfigProps;
|
||||
|
||||
constructor(props: PollEvmLogsConfigProps = { addresses: [], topics: [] }) {
|
||||
constructor(props: PollEvmLogsConfigProps) {
|
||||
if (props.fromBlock && props.toBlock && props.fromBlock > props.toBlock) {
|
||||
throw new Error("fromBlock must be less than or equal to toBlock");
|
||||
}
|
||||
|
@ -213,9 +204,7 @@ export class PollEvmLogsConfig {
|
|||
return this.props.chain;
|
||||
}
|
||||
|
||||
static fromBlock(fromBlock: bigint) {
|
||||
const cfg = new PollEvmLogsConfig();
|
||||
cfg.props.fromBlock = fromBlock;
|
||||
return cfg;
|
||||
static fromBlock(chain: string, fromBlock: bigint) {
|
||||
return new PollEvmLogsConfig({ chain, fromBlock, addresses: [], topics: [] });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
export * from "./evm/HandleEvmLogs";
|
||||
export * from "./evm/GetEvmLogs";
|
||||
export * from "./evm/PollEvmLogs";
|
||||
export * from "./solana/GetSolanaTransactions";
|
||||
export * from "./solana/PollSolanaTransactions";
|
||||
export * from "./RunPollingJob";
|
||||
export * from "./StartJobs";
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import winston from "winston";
|
||||
import { RunPollingJob } from "../RunPollingJob";
|
||||
import { MetadataRepository, SolanaSlotRepository, StatRepository } from "../../repositories";
|
||||
import { solana } from "../../entities";
|
||||
|
||||
export class GetSolanaTransactions {
|
||||
private slotRepository: SolanaSlotRepository;
|
||||
protected logger: winston.Logger;
|
||||
|
||||
constructor(slotRepo: SolanaSlotRepository) {
|
||||
this.slotRepository = slotRepo;
|
||||
this.logger = winston.child({ module: "GetSolanaTransactions" });
|
||||
}
|
||||
|
||||
async execute(
|
||||
programId: string,
|
||||
range: Range,
|
||||
opts: GetSolanaTxsOpts
|
||||
): Promise<solana.Transaction[]> {
|
||||
if (
|
||||
!range.fromBlock.blockTime ||
|
||||
!range.toBlock.blockTime ||
|
||||
range.fromBlock.blockTime > range.toBlock.blockTime
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid slot range: fromSlotBlockTime=${range.fromBlock.blockTime} toSlotBlockTime=${range.toBlock.blockTime}`
|
||||
);
|
||||
}
|
||||
|
||||
// signatures for address goes back from current sig
|
||||
const afterSignature = range.fromBlock.transactions[0]?.transaction.signatures[0];
|
||||
let beforeSignature: string | undefined =
|
||||
range.toBlock.transactions[range.toBlock.transactions.length - 1]?.transaction.signatures[0];
|
||||
if (!afterSignature || !beforeSignature) {
|
||||
throw new Error(
|
||||
`No signature presents in transactions. After: ${afterSignature}. Before: ${beforeSignature} [slots: ${range.fromBlock.blockTime} - ${range.toBlock.blockTime}]`
|
||||
);
|
||||
}
|
||||
|
||||
let currentSignaturesCount = opts.signaturesLimit;
|
||||
|
||||
let results: solana.Transaction[] = [];
|
||||
while (currentSignaturesCount === opts.signaturesLimit && beforeSignature != undefined) {
|
||||
const sigs: solana.ConfirmedSignatureInfo[] =
|
||||
await this.slotRepository.getSignaturesForAddress(
|
||||
programId,
|
||||
beforeSignature,
|
||||
afterSignature,
|
||||
opts.signaturesLimit,
|
||||
opts.commitment
|
||||
);
|
||||
this.logger.debug(
|
||||
`Got ${sigs.length} signatures for address ${programId} [blocks: ${
|
||||
range.fromBlock.blockTime
|
||||
} - ${range.toBlock.blockTime}][sigs: ${afterSignature.substring(
|
||||
0,
|
||||
5
|
||||
)} - ${beforeSignature.substring(0, 5)}]]`
|
||||
);
|
||||
|
||||
const txs = await this.slotRepository.getTransactions(sigs, opts.commitment);
|
||||
results.push(...txs);
|
||||
currentSignaturesCount = sigs.length;
|
||||
beforeSignature = sigs.at(-1)?.signature;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
export type GetSolanaTxsOpts = {
|
||||
commitment: string;
|
||||
signaturesLimit: number;
|
||||
};
|
||||
|
||||
type Range = {
|
||||
fromBlock: solana.Block;
|
||||
toBlock: solana.Block;
|
||||
};
|
|
@ -1,5 +1,6 @@
|
|||
import winston from "winston";
|
||||
import { RunPollingJob } from "../RunPollingJob";
|
||||
import { GetSolanaTransactions } from "./GetSolanaTransactions";
|
||||
import { MetadataRepository, SolanaSlotRepository, StatRepository } from "../../repositories";
|
||||
import { solana } from "../../entities";
|
||||
|
||||
|
@ -8,6 +9,7 @@ export class PollSolanaTransactions extends RunPollingJob {
|
|||
private metadataRepo: MetadataRepository<PollSolanaTransactionsMetadata>;
|
||||
private slotRepository: SolanaSlotRepository;
|
||||
private statsRepo: StatRepository;
|
||||
private getSolanaTransactions: GetSolanaTransactions;
|
||||
private latestSlot?: number;
|
||||
private slotCursor?: number;
|
||||
private lastRange?: Range;
|
||||
|
@ -23,6 +25,7 @@ export class PollSolanaTransactions extends RunPollingJob {
|
|||
|
||||
this.metadataRepo = metadataRepo;
|
||||
this.slotRepository = slotRepo;
|
||||
this.getSolanaTransactions = new GetSolanaTransactions(slotRepo);
|
||||
this.statsRepo = statsRepo;
|
||||
this.cfg = cfg;
|
||||
this.logger = winston.child({ module: "PollSolanaTransactions", label: this.cfg.id });
|
||||
|
@ -78,32 +81,14 @@ export class PollSolanaTransactions extends RunPollingJob {
|
|||
);
|
||||
}
|
||||
|
||||
let currentSignaturesCount = this.cfg.signaturesLimit;
|
||||
|
||||
let results: solana.Transaction[] = [];
|
||||
while (currentSignaturesCount === this.cfg.signaturesLimit && beforeSignature != undefined) {
|
||||
const sigs: solana.ConfirmedSignatureInfo[] =
|
||||
await this.slotRepository.getSignaturesForAddress(
|
||||
this.cfg.programId,
|
||||
beforeSignature,
|
||||
afterSignature,
|
||||
this.cfg.signaturesLimit,
|
||||
this.cfg.commitment
|
||||
);
|
||||
this.logger.debug(
|
||||
`Got ${sigs.length} signatures for address ${this.cfg.programId} [slots: ${
|
||||
range.fromSlot
|
||||
} - ${range.toSlot}][sigs: ${afterSignature.substring(0, 5)} - ${beforeSignature.substring(
|
||||
0,
|
||||
5
|
||||
)}]]`
|
||||
);
|
||||
|
||||
const txs = await this.slotRepository.getTransactions(sigs, this.cfg.commitment);
|
||||
results.push(...txs);
|
||||
currentSignaturesCount = sigs.length;
|
||||
beforeSignature = sigs.at(-1)?.signature;
|
||||
}
|
||||
const results: solana.Transaction[] = await this.getSolanaTransactions.execute(
|
||||
this.cfg.programId,
|
||||
{ fromBlock, toBlock },
|
||||
{
|
||||
commitment: this.cfg.commitment,
|
||||
signaturesLimit: this.cfg.signaturesLimit,
|
||||
}
|
||||
);
|
||||
|
||||
this.lastRange = range;
|
||||
return results;
|
||||
|
|
|
@ -4,9 +4,9 @@ import { ConfirmedSignatureInfo } from "./entities/solana";
|
|||
import { Fallible, SolanaFailure } from "./errors";
|
||||
|
||||
export interface EvmBlockRepository {
|
||||
getBlockHeight(finality: string): Promise<bigint>;
|
||||
getBlocks(blockNumbers: Set<bigint>): Promise<Record<string, EvmBlock>>;
|
||||
getFilteredLogs(filter: EvmLogFilter): Promise<EvmLog[]>;
|
||||
getBlockHeight(chain: string, finality: string): Promise<bigint>;
|
||||
getBlocks(chain: string, blockNumbers: Set<bigint>): Promise<Record<string, EvmBlock>>;
|
||||
getFilteredLogs(chain: string, filter: EvmLogFilter): Promise<EvmLog[]>;
|
||||
}
|
||||
|
||||
export interface SolanaSlotRepository {
|
||||
|
|
|
@ -13,11 +13,11 @@ export type Config = {
|
|||
jobs: {
|
||||
dir: string;
|
||||
};
|
||||
platforms: Record<string, PlatformConfig>;
|
||||
chains: Record<string, ChainRPCConfig>;
|
||||
supportedChains: string[];
|
||||
};
|
||||
|
||||
export type PlatformConfig = {
|
||||
export type ChainRPCConfig = {
|
||||
name: string;
|
||||
network: string;
|
||||
chainId: number;
|
||||
|
@ -49,6 +49,6 @@ export const configuration = {
|
|||
jobs: {
|
||||
dir: config.get<string>("jobs.dir"),
|
||||
},
|
||||
platforms: config.get<Record<string, PlatformConfig>>("platforms"),
|
||||
chains: config.get<Record<string, ChainRPCConfig>>("chains"),
|
||||
supportedChains: config.get<string[]>("supportedChains"),
|
||||
} as Config;
|
||||
|
|
|
@ -5,7 +5,7 @@ import { solana, LogFoundEvent, LogMessagePublished } from "../../domain/entitie
|
|||
import { CompiledInstruction, MessageCompiledInstruction } from "../../domain/entities/solana";
|
||||
import { configuration } from "../config";
|
||||
|
||||
const connection = new Connection(configuration.platforms.solana.rpcs[0]); // TODO: should be better to inject this to improve testability
|
||||
const connection = new Connection(configuration.chains.solana.rpcs[0]); // TODO: should be better to inject this to improve testability
|
||||
|
||||
export const solanaLogMessagePublishedMapper = async (
|
||||
tx: solana.Transaction,
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { EvmBlock, EvmLogFilter, EvmLog, EvmTag } from "../../domain/entities";
|
||||
import { EvmBlockRepository } from "../../domain/repositories";
|
||||
import winston from "../log";
|
||||
import { HttpClient } from "../http/HttpClient";
|
||||
import { HttpClient } from "../rpc/http/HttpClient";
|
||||
import { HttpClientError } from "../errors/HttpClientError";
|
||||
import { ChainRPCConfig } from "../config";
|
||||
|
||||
/**
|
||||
* EvmJsonRPCBlockRepository is a repository that uses a JSON RPC endpoint to fetch blocks.
|
||||
|
@ -13,20 +14,19 @@ const HEXADECIMAL_PREFIX = "0x";
|
|||
|
||||
export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
||||
private httpClient: HttpClient;
|
||||
private chainId: number;
|
||||
private rpc: URL;
|
||||
private cfg: EvmJsonRPCBlockRepositoryCfg;
|
||||
private readonly logger;
|
||||
|
||||
constructor(cfg: EvmJsonRPCBlockRepositoryCfg, httpClient: HttpClient) {
|
||||
this.httpClient = httpClient;
|
||||
this.chainId = cfg.chainId;
|
||||
this.rpc = new URL(cfg.rpc);
|
||||
this.logger = winston.child({ module: "EvmJsonRPCBlockRepository", chain: cfg.chain });
|
||||
this.logger.info(`Using RPC node ${this.rpc.hostname}`);
|
||||
this.cfg = cfg;
|
||||
|
||||
this.logger = winston.child({ module: "EvmJsonRPCBlockRepository" });
|
||||
this.logger.info(`Created for ${Object.keys(this.cfg.chains)}`);
|
||||
}
|
||||
|
||||
async getBlockHeight(finality: EvmTag): Promise<bigint> {
|
||||
const block: EvmBlock = await this.getBlock(finality);
|
||||
async getBlockHeight(chain: string, finality: EvmTag): Promise<bigint> {
|
||||
const block: EvmBlock = await this.getBlock(chain, finality);
|
||||
return block.number;
|
||||
}
|
||||
|
||||
|
@ -35,7 +35,7 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
|||
* @param blockNumbers
|
||||
* @returns a record of block hash -> EvmBlock
|
||||
*/
|
||||
async getBlocks(blockNumbers: Set<bigint>): Promise<Record<string, EvmBlock>> {
|
||||
async getBlocks(chain: string, blockNumbers: Set<bigint>): Promise<Record<string, EvmBlock>> {
|
||||
if (!blockNumbers.size) return {};
|
||||
|
||||
const reqs: any[] = [];
|
||||
|
@ -51,11 +51,12 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
|||
});
|
||||
}
|
||||
|
||||
const chainCfg = this.getCurrentChain(chain);
|
||||
let results: (undefined | { id: string; result?: EvmBlock; error?: ErrorBlock })[];
|
||||
try {
|
||||
results = await this.httpClient.post<typeof results>(this.rpc.href, reqs);
|
||||
results = await this.httpClient.post<typeof results>(chainCfg.rpc.href, reqs);
|
||||
} catch (e: HttpClientError | any) {
|
||||
this.handleError(e, "eth_getBlockByNumber");
|
||||
this.handleError(chain, e, "eth_getBlockByNumber");
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
@ -72,7 +73,6 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
|||
(response && response.result === null) ||
|
||||
(response?.error && response.error?.code && response.error.code === 6969)
|
||||
) {
|
||||
this.logger.warn;
|
||||
return {
|
||||
hash: "",
|
||||
number: BigInt(response.id),
|
||||
|
@ -92,14 +92,16 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
|||
};
|
||||
}
|
||||
|
||||
const msg = `[getBlocks] Got error ${
|
||||
const msg = `[${chain}][getBlocks] Got error ${
|
||||
response?.error?.message
|
||||
} for eth_getBlockByNumber for ${response?.id ?? reqs[idx].id} on ${this.rpc.hostname}`;
|
||||
} for eth_getBlockByNumber for ${response?.id ?? reqs[idx].id} on ${
|
||||
chainCfg.rpc.hostname
|
||||
}`;
|
||||
|
||||
this.logger.error(msg);
|
||||
|
||||
throw new Error(
|
||||
`Unable to parse result of eth_getBlockByNumber for ${
|
||||
`Unable to parse result of eth_getBlockByNumber[${chain}] for ${
|
||||
response?.id ?? reqs[idx].id
|
||||
}: ${msg}`
|
||||
);
|
||||
|
@ -114,11 +116,11 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
|||
throw new Error(
|
||||
`Unable to parse ${
|
||||
results?.length ?? 0
|
||||
} blocks for eth_getBlockByNumber for numbers ${blockNumbers} on ${this.rpc.hostname}`
|
||||
} blocks for eth_getBlockByNumber for numbers ${blockNumbers} on ${chainCfg.rpc.hostname}`
|
||||
);
|
||||
}
|
||||
|
||||
async getFilteredLogs(filter: EvmLogFilter): Promise<EvmLog[]> {
|
||||
async getFilteredLogs(chain: string, filter: EvmLogFilter): Promise<EvmLog[]> {
|
||||
const parsedFilters = {
|
||||
topics: filter.topics,
|
||||
address: filter.addresses,
|
||||
|
@ -126,31 +128,32 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
|||
toBlock: `${HEXADECIMAL_PREFIX}${filter.toBlock.toString(16)}`,
|
||||
};
|
||||
|
||||
const chainCfg = this.getCurrentChain(chain);
|
||||
let response: { result: Log[]; error?: ErrorBlock };
|
||||
try {
|
||||
response = await this.httpClient.post<typeof response>(this.rpc.href, {
|
||||
response = await this.httpClient.post<typeof response>(chainCfg.rpc.href, {
|
||||
jsonrpc: "2.0",
|
||||
method: "eth_getLogs",
|
||||
params: [parsedFilters],
|
||||
id: 1,
|
||||
});
|
||||
} catch (e: HttpClientError | any) {
|
||||
this.handleError(e, "eth_getLogs");
|
||||
this.handleError(chain, e, "eth_getLogs");
|
||||
throw e;
|
||||
}
|
||||
|
||||
const logs = response?.result;
|
||||
this.logger.info(
|
||||
`[getFilteredLogs] Got ${logs?.length} logs for ${this.describeFilter(filter)} from ${
|
||||
this.rpc.hostname
|
||||
}`
|
||||
`[${chain}][getFilteredLogs] Got ${logs?.length} logs for ${this.describeFilter(
|
||||
filter
|
||||
)} from ${chainCfg.rpc.hostname}`
|
||||
);
|
||||
|
||||
return logs.map((log) => ({
|
||||
...log,
|
||||
blockNumber: BigInt(log.blockNumber),
|
||||
transactionIndex: log.transactionIndex.toString(),
|
||||
chainId: this.chainId,
|
||||
chainId: chainCfg.chainId,
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -161,17 +164,18 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
|||
/**
|
||||
* Loosely based on the wormhole-dashboard implementation (minus some specially crafted blocks when null result is obtained)
|
||||
*/
|
||||
private async getBlock(blockNumberOrTag: EvmTag): Promise<EvmBlock> {
|
||||
private async getBlock(chain: string, blockNumberOrTag: EvmTag): Promise<EvmBlock> {
|
||||
const chainCfg = this.getCurrentChain(chain);
|
||||
let response: { result?: EvmBlock; error?: ErrorBlock };
|
||||
try {
|
||||
response = await this.httpClient.post<typeof response>(this.rpc.href, {
|
||||
response = await this.httpClient.post<typeof response>(chainCfg.rpc.href, {
|
||||
jsonrpc: "2.0",
|
||||
method: "eth_getBlockByNumber",
|
||||
params: [blockNumberOrTag, false], // this means we'll get a light block (no txs)
|
||||
id: 1,
|
||||
});
|
||||
} catch (e: HttpClientError | any) {
|
||||
this.handleError(e, "eth_getBlockByNumber");
|
||||
this.handleError(chain, e, "eth_getBlockByNumber");
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
@ -186,28 +190,36 @@ export class EvmJsonRPCBlockRepository implements EvmBlockRepository {
|
|||
};
|
||||
}
|
||||
throw new Error(
|
||||
`Unable to parse result of eth_getBlockByNumber for ${blockNumberOrTag} on ${this.rpc}`
|
||||
`Unable to parse result of eth_getBlockByNumber for ${blockNumberOrTag} on ${chainCfg.rpc}`
|
||||
);
|
||||
}
|
||||
|
||||
private handleError(e: any, method: string) {
|
||||
private handleError(chain: string, e: any, method: string) {
|
||||
const chainCfg = this.getCurrentChain(chain);
|
||||
if (e instanceof HttpClientError) {
|
||||
this.logger.error(
|
||||
`[getBlock] Got ${e.status} from ${this.rpc.hostname}/${method}. ${
|
||||
`[${chain}][getBlock] Got ${e.status} from ${chainCfg.rpc.hostname}/${method}. ${
|
||||
e?.message ?? `${e?.message}`
|
||||
}`
|
||||
);
|
||||
} else {
|
||||
this.logger.error(`[getBlock] Got error ${e} from ${this.rpc.hostname}/${method}`);
|
||||
this.logger.error(
|
||||
`[${chain}][getBlock] Got error ${e} from ${chainCfg.rpc.hostname}/${method}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private getCurrentChain(chain: string) {
|
||||
const cfg = this.cfg.chains[chain];
|
||||
return {
|
||||
chainId: cfg.chainId,
|
||||
rpc: new URL(cfg.rpcs[0]),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export type EvmJsonRPCBlockRepositoryCfg = {
|
||||
rpc: string;
|
||||
timeout?: number;
|
||||
chain: string;
|
||||
chainId: number;
|
||||
chains: Record<string, ChainRPCConfig>;
|
||||
};
|
||||
|
||||
type ErrorBlock = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SNSClient, SNSClientConfig } from "@aws-sdk/client-sns";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { Config } from "./config";
|
||||
import { Config } from "../config";
|
||||
import {
|
||||
SnsEventRepository,
|
||||
EvmJsonRPCBlockRepository,
|
||||
|
@ -10,9 +10,9 @@ import {
|
|||
StaticJobRepository,
|
||||
Web3SolanaSlotRepository,
|
||||
RateLimitedSolanaSlotRepository,
|
||||
} from "./repositories";
|
||||
import { HttpClient } from "./http/HttpClient";
|
||||
import { JobRepository } from "../domain/repositories";
|
||||
} from ".";
|
||||
import { HttpClient } from "../rpc/http/HttpClient";
|
||||
import { JobRepository } from "../../domain/repositories";
|
||||
|
||||
const SOLANA_CHAIN = "solana";
|
||||
const EVM_CHAINS = ["ethereum", "avalanche", "fantom", "karura", "acala"];
|
||||
|
@ -37,10 +37,10 @@ export class RepositoriesBuilder {
|
|||
this.repositories.set("metadata", new FileMetadataRepository(this.cfg.metadata.dir));
|
||||
|
||||
this.cfg.supportedChains.forEach((chain) => {
|
||||
if (!this.cfg.platforms[chain]) throw new Error(`No config for chain ${chain}`);
|
||||
if (!this.cfg.chains[chain]) throw new Error(`No config for chain ${chain}`);
|
||||
|
||||
if (chain === SOLANA_CHAIN) {
|
||||
const cfg = this.cfg.platforms[chain];
|
||||
const cfg = this.cfg.chains[chain];
|
||||
const solanaSlotRepository = new RateLimitedSolanaSlotRepository(
|
||||
new Web3SolanaSlotRepository(
|
||||
new Connection(cfg.rpcs[0], { disableRetryOnRateLimit: true })
|
||||
|
@ -50,18 +50,12 @@ export class RepositoriesBuilder {
|
|||
this.repositories.set("solana-slotRepo", solanaSlotRepository);
|
||||
}
|
||||
|
||||
if (EVM_CHAINS.includes(chain)) {
|
||||
const httpClient = this.createHttpClient(this.cfg.platforms[chain].timeout);
|
||||
if (!this.repositories.has("evmRepo")) {
|
||||
const httpClient = this.createHttpClient(this.cfg.chains[chain].timeout);
|
||||
const repoCfg: EvmJsonRPCBlockRepositoryCfg = {
|
||||
chain,
|
||||
chainId: this.cfg.platforms[chain].chainId,
|
||||
rpc: this.cfg.platforms[chain].rpcs[0],
|
||||
timeout: this.cfg.platforms[chain].timeout,
|
||||
chains: this.cfg.chains,
|
||||
};
|
||||
this.repositories.set(
|
||||
`${chain}-evmRepo`,
|
||||
new EvmJsonRPCBlockRepository(repoCfg, httpClient)
|
||||
);
|
||||
this.repositories.set("evmRepo", new EvmJsonRPCBlockRepository(repoCfg, httpClient));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -82,7 +76,8 @@ export class RepositoriesBuilder {
|
|||
}
|
||||
|
||||
public getEvmBlockRepository(chain: string): EvmJsonRPCBlockRepository {
|
||||
return this.getRepo(`${chain}-evmRepo`);
|
||||
if (!EVM_CHAINS.includes(chain)) throw new Error(`Chain ${chain} not supported`);
|
||||
return this.getRepo("evmRepo");
|
||||
}
|
||||
|
||||
public getSnsEventRepository(): SnsEventRepository {
|
|
@ -1,4 +1,4 @@
|
|||
import { StatRepository } from "../../domain/repositories";
|
||||
import { StatRepository } from "../../../domain/repositories";
|
||||
|
||||
export class HealthController {
|
||||
private readonly statsRepo: StatRepository;
|
|
@ -1,6 +1,6 @@
|
|||
import axios, { AxiosError, AxiosInstance } from "axios";
|
||||
import { setTimeout } from "timers/promises";
|
||||
import { HttpClientError } from "../errors/HttpClientError";
|
||||
import { HttpClientError } from "../../errors/HttpClientError";
|
||||
|
||||
/**
|
||||
* A simple HTTP client with exponential backoff retries and 429 handling.
|
|
@ -1,7 +1,7 @@
|
|||
import http from "http";
|
||||
import url from "url";
|
||||
import { HealthController } from "./HealthController";
|
||||
import log from "../log";
|
||||
import log from "../../log";
|
||||
|
||||
export class WebServer {
|
||||
private server: http.Server;
|
|
@ -1,8 +1,8 @@
|
|||
import { configuration } from "./infrastructure/config";
|
||||
import { RepositoriesBuilder } from "./infrastructure/RepositoriesBuilder";
|
||||
import { RepositoriesBuilder } from "./infrastructure/repositories/RepositoriesBuilder";
|
||||
import log from "./infrastructure/log";
|
||||
import { WebServer } from "./infrastructure/rpc/Server";
|
||||
import { HealthController } from "./infrastructure/rpc/HealthController";
|
||||
import { WebServer } from "./infrastructure/rpc/http/Server";
|
||||
import { HealthController } from "./infrastructure/rpc/http/HealthController";
|
||||
import { StartJobs } from "./domain/actions";
|
||||
|
||||
let repos: RepositoriesBuilder;
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
} from "../../../../src/domain/repositories";
|
||||
import { EvmBlock, EvmLog } from "../../../../src/domain/entities";
|
||||
|
||||
let cfg = PollEvmLogsConfig.fromBlock(0n);
|
||||
let cfg = PollEvmLogsConfig.fromBlock("acala", 0n);
|
||||
|
||||
let getBlocksSpy: jest.SpiedFunction<EvmBlockRepository["getBlocks"]>;
|
||||
let getLogsSpy: jest.SpiedFunction<EvmBlockRepository["getFilteredLogs"]>;
|
||||
|
@ -46,9 +46,13 @@ describe("PollEvmLogs", () => {
|
|||
|
||||
await thenWaitForAssertion(
|
||||
() => expect(getBlocksSpy).toHaveReturnedTimes(1),
|
||||
() => expect(getBlocksSpy).toHaveBeenCalledWith(new Set([currentHeight, currentHeight + 1n])),
|
||||
() =>
|
||||
expect(getLogsSpy).toBeCalledWith({
|
||||
expect(getBlocksSpy).toHaveBeenCalledWith(
|
||||
"acala",
|
||||
new Set([currentHeight, currentHeight + 1n])
|
||||
),
|
||||
() =>
|
||||
expect(getLogsSpy).toBeCalledWith("acala", {
|
||||
addresses: cfg.addresses,
|
||||
topics: cfg.topics,
|
||||
fromBlock: currentHeight + blocksAhead,
|
||||
|
@ -73,7 +77,7 @@ describe("PollEvmLogs", () => {
|
|||
new Set([lastExtractedBlock, lastExtractedBlock + 1n])
|
||||
),
|
||||
() =>
|
||||
expect(getLogsSpy).toBeCalledWith({
|
||||
expect(getLogsSpy).toBeCalledWith("acala", {
|
||||
addresses: cfg.addresses,
|
||||
topics: cfg.topics,
|
||||
fromBlock: lastExtractedBlock + 1n,
|
||||
|
|
|
@ -3,9 +3,10 @@ import { EvmJsonRPCBlockRepository } from "../../../src/infrastructure/repositor
|
|||
import axios from "axios";
|
||||
import nock from "nock";
|
||||
import { EvmLogFilter, EvmTag } from "../../../src/domain/entities";
|
||||
import { HttpClient } from "../../../src/infrastructure/http/HttpClient";
|
||||
import { HttpClient } from "../../../src/infrastructure/rpc/http/HttpClient";
|
||||
|
||||
axios.defaults.adapter = "http"; // needed by nock
|
||||
const eth = "ethereum";
|
||||
const rpc = "http://localhost";
|
||||
const address = "0x98f3c9e6e3face36baad05fe09d375ef1464288b";
|
||||
const topic = "0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2";
|
||||
|
@ -27,7 +28,7 @@ describe("EvmJsonRPCBlockRepository", () => {
|
|||
givenARepo();
|
||||
givenBlockHeightIs(expectedHeight, "latest");
|
||||
|
||||
const result = await repo.getBlockHeight("latest");
|
||||
const result = await repo.getBlockHeight(eth, "latest");
|
||||
|
||||
expect(result).toBe(expectedHeight);
|
||||
});
|
||||
|
@ -37,7 +38,7 @@ describe("EvmJsonRPCBlockRepository", () => {
|
|||
givenARepo();
|
||||
givenBlocksArePresent(blockNumbers);
|
||||
|
||||
const result = await repo.getBlocks(new Set(blockNumbers));
|
||||
const result = await repo.getBlocks(eth, new Set(blockNumbers));
|
||||
|
||||
expect(Object.keys(result)).toHaveLength(blockNumbers.length);
|
||||
blockNumbers.forEach((blockNumber) => {
|
||||
|
@ -55,7 +56,7 @@ describe("EvmJsonRPCBlockRepository", () => {
|
|||
|
||||
givenLogsPresent(filter);
|
||||
|
||||
const logs = await repo.getFilteredLogs(filter);
|
||||
const logs = await repo.getFilteredLogs(eth, filter);
|
||||
|
||||
expect(logs).toHaveLength(1);
|
||||
expect(logs[0].blockNumber).toBe(1n);
|
||||
|
@ -66,7 +67,11 @@ describe("EvmJsonRPCBlockRepository", () => {
|
|||
|
||||
const givenARepo = () => {
|
||||
repo = new EvmJsonRPCBlockRepository(
|
||||
{ rpc, timeout: 100, chain: "ethereum", chainId: 2 },
|
||||
{
|
||||
chains: {
|
||||
ethereum: { rpcs: [rpc], timeout: 100, name: "ethereum", network: "mainnet", chainId: 2 },
|
||||
},
|
||||
},
|
||||
new HttpClient()
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from "@jest/globals";
|
||||
import { RepositoriesBuilder } from "../../../src/infrastructure/RepositoriesBuilder";
|
||||
import { RepositoriesBuilder } from "../../../src/infrastructure/repositories/RepositoriesBuilder";
|
||||
import { configMock } from "../../mocks/configMock";
|
||||
import {
|
||||
EvmJsonRPCBlockRepository,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { SnsConfig } from "../../src/infrastructure/repositories";
|
||||
import { Config, PlatformConfig } from "../../src/infrastructure/config";
|
||||
import { Config, ChainRPCConfig } from "../../src/infrastructure/config";
|
||||
|
||||
export const configMock = (chains: string[] = []): Config => {
|
||||
const platformRecord: Record<string, PlatformConfig> = {
|
||||
const platformRecord: Record<string, ChainRPCConfig> = {
|
||||
solana: {
|
||||
name: "solana",
|
||||
network: "devnet",
|
||||
|
@ -71,7 +71,7 @@ export const configMock = (chains: string[] = []): Config => {
|
|||
jobs: {
|
||||
dir: "./metadata-repo/jobs",
|
||||
},
|
||||
platforms: platformRecord,
|
||||
chains: platformRecord,
|
||||
supportedChains: chains,
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
NODE_ENV=prod-mainnet
|
||||
NODE_ENV=mainnet
|
||||
BLOCKCHAIN_ENV=mainnet
|
||||
NAMESPACE=wormscan
|
||||
NAME=blockchain-watcher
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
NODE_ENV=prod-testnet
|
||||
NODE_ENV=testnet
|
||||
BLOCKCHAIN_ENV=testnet
|
||||
NAMESPACE=wormscan-testnet
|
||||
NAME=blockchain-watcher
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
NODE_ENV=staging-mainnet
|
||||
NODE_ENV=mainnet
|
||||
BLOCKCHAIN_ENV=mainnet
|
||||
NAMESPACE=wormscan
|
||||
NAME=blockchain-watcher
|
||||
|
|
|
@ -47,8 +47,7 @@ data:
|
|||
"commitment": "latest",
|
||||
"interval": 15000,
|
||||
"addresses": ["0x706abc4E45D419950511e474C7B9Ed348A4a716c"],
|
||||
"chain": "ethereum",
|
||||
"topics": []
|
||||
"chain": "ethereum"
|
||||
}
|
||||
},
|
||||
"handlers": [
|
||||
|
@ -78,8 +77,7 @@ data:
|
|||
"addresses": [
|
||||
"0xE4eacc10990ba3308DdCC72d985f2a27D20c7d03"
|
||||
],
|
||||
"chain": "karura",
|
||||
"topics": []
|
||||
"chain": "karura"
|
||||
}
|
||||
},
|
||||
"handlers": [
|
||||
|
@ -113,8 +111,7 @@ data:
|
|||
"addresses": [
|
||||
"0x1BB3B4119b7BA9dfad76B0545fb3F531383c3bB7"
|
||||
],
|
||||
"chain": "fantom",
|
||||
"topics": []
|
||||
"chain": "fantom"
|
||||
}
|
||||
},
|
||||
"handlers": [
|
||||
|
@ -148,8 +145,7 @@ data:
|
|||
"addresses": [
|
||||
"0x4377B49d559c0a9466477195C6AdC3D433e265c0"
|
||||
],
|
||||
"chain": "acala",
|
||||
"topics": []
|
||||
"chain": "acala"
|
||||
}
|
||||
},
|
||||
"handlers": [
|
||||
|
@ -219,8 +215,7 @@ data:
|
|||
"commitment": "latest",
|
||||
"interval": 5000,
|
||||
"addresses": ["0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B"],
|
||||
"chain": "ethereum",
|
||||
"topics": []
|
||||
"chain": "ethereum"
|
||||
}
|
||||
},
|
||||
"handlers": [
|
||||
|
@ -250,8 +245,7 @@ data:
|
|||
"addresses": [
|
||||
"0xa321448d90d4e5b0A732867c18eA198e75CAC48E"
|
||||
],
|
||||
"chain": "karura",
|
||||
"topics": []
|
||||
"chain": "karura"
|
||||
}
|
||||
},
|
||||
"handlers": [
|
||||
|
@ -285,8 +279,7 @@ data:
|
|||
"addresses": [
|
||||
"0x126783A6Cb203a3E35344528B26ca3a0489a1485"
|
||||
],
|
||||
"chain": "fantom",
|
||||
"topics": []
|
||||
"chain": "fantom"
|
||||
}
|
||||
},
|
||||
"handlers": [
|
||||
|
@ -320,8 +313,7 @@ data:
|
|||
"addresses": [
|
||||
"0xa321448d90d4e5b0A732867c18eA198e75CAC48E"
|
||||
],
|
||||
"chain": "acala",
|
||||
"topics": []
|
||||
"chain": "acala"
|
||||
}
|
||||
},
|
||||
"handlers": [
|
||||
|
|
Loading…
Reference in New Issue