UNPKG

ssv-scanner

Version:

Tool for retrieving events data (cluster snapshots and owner nonce) from the SSV network contract.

9 lines (8 loc) 11.9 kB
#!/usr/bin/env node "use strict";var z=Object.create;var J=Object.defineProperty;var G=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var X=Object.getPrototypeOf,Z=Object.prototype.hasOwnProperty;var ee=(n,e,r,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Q(e))!Z.call(n,s)&&s!==r&&J(n,s,{get:()=>e[s],enumerable:!(t=G(e,s))||t.enumerable});return n};var x=(n,e,r)=>(r=n!=null?z(X(n)):{},ee(e||!n||!n.__esModule?J(r,"default",{value:n,enumerable:!0}):r,n));var Y=x(require("figlet"));var D={name:"ssv-scanner",version:"1.1.1",description:"Tool for retrieving events data (cluster snapshots and owner nonce) from the SSV network contract.",author:"SSV.Network",repository:"https://github.com/bloxapp/ssv-scanner",license:"MIT",keywords:["ssv","ssv.network","cluster","nonce","scanner"],main:"./dist/tsc/src/main.js",types:"./dist/tsc/src/main.d.ts",bin:{"ssv-keys":"./dist/tsc/src/cli.js"},engines:{node:">=18"},scripts:{"dev:cli":"ts-node src/cli.ts",cli:"node ./dist/tsc/src/cli.js",lint:"eslint src/ --ext .js,.jsx,.ts,.tsx",clean:"rm -rf dist build package","ts-node":"ts-node","copy-json":"cpy './src/shared/abi/*.json' './dist/tsc/src/shared/abi/'",build:"tsc -p tsconfig.json","build-all":"yarn clean && yarn build && yarn copy-json && yarn esbuild",esbuild:"node ./esbuild.js","pre-commit":"yarn test && yarn lint && yarn build-all"},devDependencies:{"@types/argparse":"^2.0.10","@types/cli-progress":"^3.11.0","@types/node":"^15.14.9","cpy-cli":"^5.0.0",esbuild:"^0.14.38","esbuild-node-externals":"^1.4.1",eslint:"^7.32.0","ts-node":"^10.9.1",typescript:"^4.6.4"},dependencies:{"@types/figlet":"^1.5.4",argparse:"^2.0.1","cli-progress":"^3.11.2",ethers:"^6.13.2",figlet:"^1.5.2",yarn:"^1.22.22"},licenses:[{MIT:"SEE LICENSE IN LICENCE FILE"}]};var j=x(require("process")),R=require("argparse");var U=require("argparse"),f=class{constructor(e,r){this.name=e;this.description=r;this.env="";this.parser=new U.ArgumentParser({description:this.description}),this.setArguments(this.parser)}parse(e){e.splice(0,1);let r=e.map(s=>(s.endsWith("_stage")&&(this.env="stage",s=s.replace("_stage","")),s)),t=this.parser.parse_args(r);return this.env&&(t.network+=`_${this.env}`),t}};var V=require("ethers"),H=x(require("cli-progress"));var te={MAINNET:"prod:v4.mainnet",HOODI:"prod:v4.hoodi",HOODI_STAGE:"stage:v4.hoodi",LOCAL_TESTNET:"local:v4.testnet"},S=n=>{let[e,r]=te[n.toUpperCase()].split(":"),t;try{t=require(`../shared/abi/${e}.${r}.abi.json`)}catch(s){throw console.error(`Failed to load JSON data from ${e}.${r}.abi.json`,s),s}if(!t.contractAddress||!t.abi||!t.genesisBlock&&t.genesisBlock!==0)throw new Error(`Missing core data in JSON for ${e}.${r}`);return{contractAddress:t.contractAddress,abi:t.abi,genesisBlock:t.genesisBlock}};var F=require("ethers"),b=class{constructor(e){this.DAY=5400;this.WEEK=this.DAY*7;this.MONTH=this.DAY*30;if(!e.nodeUrl)throw Error("ETH1 node is required");if(!e.network)throw Error("Network is required");if(!e.ownerAddress)throw Error("Cluster owner address is required");if(e.ownerAddress.length!==42)throw Error("Invalid owner address length.");if(!e.ownerAddress.startsWith("0x"))throw Error("Invalid owner address.");this.params=e,this.params.ownerAddress=F.ethers.getAddress(this.params.ownerAddress)}};var N=class extends b{async run(e){e&&(console.log(` Scanning blockchain...`),this.progressBar=new H.default.SingleBar({},H.default.Presets.shades_classic));try{let r=await this._getValidatorAddedEventCount(e);return e&&this.progressBar.stop(),r}catch(r){throw e&&this.progressBar.stop(),new Error(r)}}async _getValidatorAddedEventCount(e){let{contractAddress:r,abi:t,genesisBlock:s}=S(this.params.network),a=new V.ethers.JsonRpcProvider(this.params.nodeUrl),o=new V.ethers.Contract(r,t,a),d;try{d=await a.getBlockNumber()}catch{throw new Error("Could not access the provided node endpoint.")}try{await o.owner()}catch{throw new Error("Could not find any cluster snapshot from the provided contract address.")}let g=0,i=this.MONTH;e&&this.progressBar.start(Number(d),0);let c=o.filters.ValidatorAdded(this.params.ownerAddress);for(let l=s;l<=d;l+=i)try{let p=Math.min(l+i-1,d);g+=(await o.queryFilter(c,l,p)).length,e&&this.progressBar.update(p)}catch(p){if(i===this.MONTH)i=this.WEEK;else if(i===this.WEEK)i=this.DAY;else throw new Error(p)}return e&&this.progressBar.update(d,d),g}};var P=class extends f{constructor(){super("nonce","Handles nonce operations")}setArguments(e){e.add_argument("-nw","--network",{help:"The network",choices:["mainnet","hoodi","local_testnet"],required:!0,dest:"network"}),e.add_argument("-n","--node-url",{help:"ETH1 (execution client) node endpoint url",required:!0,dest:"nodeUrl"}),e.add_argument("-oa","--owner-address",{help:"The cluster owner address (in the SSV contract)",required:!0,dest:"ownerAddress"})}async run(e){try{let t=await new N(e).run(!0);console.log("Next Nonce:",t)}catch(r){console.error("\x1B[31m",r.message)}}};var C=require("ethers"),M=x(require("cli-progress"));var I=class extends b{async run(e,r){if(!(Array.isArray(e)&&this._isValidOperatorIds(e.length)))throw Error("Comma-separated list of operator IDs. The amount must be 3f+1 compatible.");e=[...e].sort((a,o)=>a-o),r&&(console.log(` Scanning blockchain...`),this.progressBar=new M.default.SingleBar({},M.default.Presets.shades_classic));let s=await this._getClusterSnapshot(e,r);return r&&this.progressBar.stop(),s}async _getClusterSnapshot(e,r){let{contractAddress:t,abi:s,genesisBlock:a}=S(this.params.network),o,d=new C.ethers.JsonRpcProvider(this.params.nodeUrl);try{o=await d.getBlockNumber()}catch(u){throw new Error("Could not access the provided node endpoint: "+u)}let g=new C.ethers.Contract(t,s,d);try{await g.owner()}catch(u){throw new Error("Could not find any cluster snapshot from the provided contract address: "+u)}let i=this.MONTH,c,l=0,p=["ClusterDeposited","ClusterWithdrawn","ClusterReactivated","ValidatorRemoved","ValidatorAdded","ClusterLiquidated","ClusterWithdrawn"];r&&this.progressBar.start(o,a);let k=JSON.stringify(e),A=a;for(let u=o;u>a&&!c;u-=i){let m=Math.max(u-i+1,a);try{let w={address:t,fromBlock:m,toBlock:u,topics:[null,C.ethers.zeroPadValue(this.params.ownerAddress,32)]};c=(await d.getLogs(w)).map(h=>({event:g.interface.parseLog(h),blockNumber:h.blockNumber,transactionIndex:h.transactionIndex,logIndex:h.index})).filter(h=>h.event&&p.includes(h.event.name)).filter(h=>JSON.stringify(h.event?.args.operatorIds.map(y=>Number(y)))===k).sort((h,y)=>y.blockNumber===h.blockNumber?y.transactionIndex===h.transactionIndex?y.logIndex-h.logIndex:y.transactionIndex-h.transactionIndex:y.blockNumber-h.blockNumber)[0].event?.args.cluster}catch{i===this.MONTH?(i=this.WEEK,u+=this.WEEK):i===this.WEEK&&(i=this.DAY,u+=this.DAY)}A+=i,r&&this.progressBar.update(A,o)}return r&&this.progressBar.update(o,o),c=c||["0","0","0",!0,"0"],{payload:{Owner:this.params.ownerAddress,Operators:e.join(","),Block:l||o,Data:c.join(",")},cluster:{validatorCount:Number(c[0]),networkFeeIndex:c[1].toString(),index:c[2].toString(),active:c[3],balance:c[4].toString()}}}_isValidOperatorIds(e){return!(e<4||e>13||e%3!=1)}};var O=class extends f{constructor(){super("cluster","Handles cluster operations")}setArguments(e){e.add_argument("-nw","--network",{help:"The network",choices:["mainnet","hoodi","local_testnet"],required:!0,dest:"network"}),e.add_argument("-n","--node-url",{help:"ETH1 (execution client) node endpoint url",required:!0,dest:"nodeUrl"}),e.add_argument("-oa","--owner-address",{help:"The cluster owner address (in the SSV contract)",required:!0,dest:"ownerAddress"}),e.add_argument("-oids","--operator-ids",{help:"Comma-separated list of operators IDs regarding the cluster that you want to query",required:!0,dest:"operatorIds"})}async run(e){try{let r=e.operatorIds.split(",").map(a=>{if(Number.isNaN(+a))throw new Error("Operator Id should be the number");return+a}).sort((a,o)=>a-o),s=await new I(e).run(r,!0);console.table(s.payload),console.log("Cluster snapshot:"),console.table(s.cluster),console.log(JSON.stringify({block:s.payload.Block,"cluster snapshot":s.cluster,cluster:Object.values(s.cluster)},(a,o)=>typeof o=="bigint"?o.toString():o," "))}catch(r){console.error("\x1B[31m",r.message)}}};var v=require("ethers"),L=x(require("cli-progress"));var B=require("fs"),K=require("path"),T=class extends b{async run(e,r){r&&(console.log(` Scanning blockchain...`),this.progressBar=new L.default.SingleBar({},L.default.Presets.shades_classic));try{let t=await this._getOperatorPubkeys(e,r);return r&&this.progressBar.stop(),t}catch(t){throw r&&this.progressBar.stop(),new Error(t)}}async _getOperatorPubkeys(e,r){let{contractAddress:t,abi:s,genesisBlock:a}=S(this.params.network),o=new v.ethers.JsonRpcProvider(this.params.nodeUrl),d=new v.ethers.Contract(t,s,o),g=new v.ethers.Interface(s),i;try{i=await o.getBlockNumber()}catch{throw new Error("Could not access the provided node endpoint.")}try{await d.owner()}catch{throw new Error("Could not find any cluster snapshot from the provided contract address.")}let c=this.MONTH;r&&this.progressBar.start(Number(i),0);let l=d.filters.OperatorAdded(),p=[];for(let m=a;m<=i;m+=c)try{let w=Math.min(m+c-1,i),E=await d.queryFilter(l,m,w);p=[...p,...E],r&&this.progressBar.update(w)}catch(w){if(c===this.MONTH)c=this.WEEK;else if(c===this.WEEK)c=this.DAY;else throw new Error(w)}let k=e||K.join(__dirname,"../../data");B.existsSync(k)||B.mkdirSync(k,{recursive:!0});let A=new Array(p.length),u=K.join(k,`operator-pubkeys-${this.params.network}.json`);B.existsSync(u)&&B.writeFileSync(u,"");for(let m=0;m<p.length;m++){let w=g.parseLog(p[m]),E=g.decodeEventLog("OperatorAdded",p[m].data);if(w==null)throw new Error("Could not parse the log");let _="";try{_=v.AbiCoder.defaultAbiCoder().decode(["string"],E[2]).join("")}catch{_=E[2]}A[m]={id:m+1,pubkey:_}}return B.writeFileSync(u,JSON.stringify(A,null,2)),r&&this.progressBar.update(i,i),u}};var q=class extends f{constructor(){super("operator","Handles cluster operations")}setArguments(e){e.add_argument("-nw","--network",{help:"The network",choices:["mainnet","hoodi","hoodi_stage","local_testnet"],required:!0,dest:"network"}),e.add_argument("-n","--node-url",{help:"ETH1 (execution client) node endpoint url",required:!0,dest:"nodeUrl"}),e.add_argument("-oa","--owner-address",{help:"The cluster owner address (in the SSV contract)",required:!0,dest:"ownerAddress"}),e.add_argument("-o","--output-path",{help:"The output path for the operator data",required:!1,dest:"outputPath"})}async run(e){try{let r=new T(e),t=e.outputPath,s=await r.run(t,!0);console.log(` Operator data has been saved to: ${s}`)}catch(r){console.error("\x1B[31m",r.message)}}};var se=async n=>new Promise(e=>{(0,Y.default)(n,(r,t)=>{if(r)return e("");e(t)})});async function W(){let n=`SSV Scanner v${D.version}`,e=await se(n);if(e){console.log(" -----------------------------------------------------------------------------------"),console.log(`${e||n}`),console.log(" -----------------------------------------------------------------------------------");for(let p of String(D.description).match(/.{1,75}/g)||[])console.log(` ${p}`);console.log(` ----------------------------------------------------------------------------------- `)}let r=new R.ArgumentParser,t=r.add_subparsers({title:"commands",dest:"command"}),s=new O,a=new P,o=new q,d=t.add_parser(s.name,{add_help:!0}),g=t.add_parser(a.name,{add_help:!0}),i=t.add_parser(o.name,{add_help:!0}),c="",l=j.argv.slice(2);switch(l[1]&&l[1].includes("--help")?(s.setArguments(d),a.setArguments(g),o.setArguments(i),r.parse_args()):(c=r.parse_known_args()[0].command,s.setArguments(d),a.setArguments(g),o.setArguments(i)),c){case s.name:await s.run(s.parse(l));break;case a.name:await a.run(a.parse(l));break;case o.name:await o.run(o.parse(l));break;default:console.error("Command not found"),j.exit(1)}}W();