ssv-scanner
Version:
Tool for retrieving events data (cluster snapshots and owner nonce) from the SSV network contract.
9 lines (8 loc) • 12.1 kB
JavaScript
;var G=Object.create;var J=Object.defineProperty;var z=Object.getOwnPropertyDescriptor;var Q=Object.getOwnPropertyNames;var X=Object.getPrototypeOf,Z=Object.prototype.hasOwnProperty;var ee=(a,e,r,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let t of Q(e))!Z.call(a,t)&&t!==r&&J(a,t,{get:()=>e[t],enumerable:!(s=z(e,t))||s.enumerable});return a};var x=(a,e,r)=>(r=a!=null?G(X(a)):{},ee(e||!a||!a.__esModule?J(r,"default",{value:a,enumerable:!0}):r,a));var Y=x(require("figlet"));var D={name:"ssv-scanner",version:"1.0.4",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"},licenses:[{MIT:"SEE LICENSE IN LICENCE FILE"}]};var q=x(require("process")),R=require("argparse");var K=require("argparse"),f=class{constructor(e,r){this.name=e;this.description=r;this.env="";this.parser=new K.ArgumentParser({description:this.description}),this.setArguments(this.parser)}parse(e){e.splice(0,1);let r=e.map(t=>(t.endsWith("_stage")&&(this.env="stage",t=t.replace("_stage","")),t)),s=this.parser.parse_args(r);return this.env&&(s.network+=`_${this.env}`),s}};var V=require("ethers"),H=x(require("cli-progress"));var te={MAINNET:"prod:v4.mainnet",HOLESKY:"prod:v4.holesky",HOLESKY_STAGE:"stage:v4.holesky",HOODI:"prod:v4.hoodi",HOODI_STAGE:"stage:v4.hoodi"},S=a=>{let[e,r]=te[a.toUpperCase()].split(":"),s;try{s=require(`../shared/abi/${e}.${r}.abi.json`)}catch(o){throw console.error(`Failed to load JSON data from ${e}.${r}.abi.json`,o),o}let t;try{t=require(`../shared/abi/${e}.${r}.abi.json`)}catch(o){throw console.error(`Failed to load JSON data from ${e}.${r}.abi.json`,o),o}if(!s.contractAddress||!s.abi||!s.genesisBlock)throw new Error(`Missing core data in JSON for ${e}.${r}`);if(!t.contractAddress||!t.abi)throw new Error(`Missing views 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 P=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:s,genesisBlock:t}=S(this.params.network),o=new V.ethers.JsonRpcProvider(this.params.nodeUrl),n=new V.ethers.Contract(r,s,o),d;try{d=await o.getBlockNumber()}catch{throw new Error("Could not access the provided node endpoint.")}try{await n.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=n.filters.ValidatorAdded(this.params.ownerAddress);for(let l=t;l<=d;l+=i)try{let p=Math.min(l+i-1,d);g+=(await n.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 _=class extends f{constructor(){super("nonce","Handles nonce operations")}setArguments(e){e.add_argument("-nw","--network",{help:"The network",choices:["mainnet","holesky","hoodi"],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 s=await new P(e).run(!0);console.log("Next Nonce:",s)}catch(r){console.error("\x1B[31m",r.message)}}};var C=require("ethers"),$=x(require("cli-progress"));var O=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((o,n)=>o-n),r&&(console.log(`
Scanning blockchain...`),this.progressBar=new $.default.SingleBar({},$.default.Presets.shades_classic));let t=await this._getClusterSnapshot(e,r);return r&&this.progressBar.stop(),t}async _getClusterSnapshot(e,r){let{contractAddress:s,abi:t,genesisBlock:o}=S(this.params.network),n,d=new C.ethers.JsonRpcProvider(this.params.nodeUrl);try{n=await d.getBlockNumber()}catch(u){throw new Error("Could not access the provided node endpoint: "+u)}let g=new C.ethers.Contract(s,t,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(n,o);let k=JSON.stringify(e),A=o;for(let u=n;u>o&&!c;u-=i){let m=Math.max(u-i+1,o);try{let w={address:s,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,n)}return r&&this.progressBar.update(n,n),c=c||["0","0","0",!0,"0"],{payload:{Owner:this.params.ownerAddress,Operators:e.join(","),Block:l||n,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 I=class extends f{constructor(){super("cluster","Handles cluster operations")}setArguments(e){e.add_argument("-nw","--network",{help:"The network",choices:["mainnet","holesky","hoodi"],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(o=>{if(Number.isNaN(+o))throw new Error("Operator Id should be the number");return+o}).sort((o,n)=>o-n),t=await new O(e).run(r,!0);console.table(t.payload),console.log("Cluster snapshot:"),console.table(t.cluster),console.log(JSON.stringify({block:t.payload.Block,"cluster snapshot":t.cluster,cluster:Object.values(t.cluster)},(o,n)=>typeof n=="bigint"?n.toString():n," "))}catch(r){console.error("\x1B[31m",r.message)}}};var v=require("ethers"),M=x(require("cli-progress"));var B=require("fs"),U=require("path"),T=class extends b{async run(e,r){r&&(console.log(`
Scanning blockchain...`),this.progressBar=new M.default.SingleBar({},M.default.Presets.shades_classic));try{let s=await this._getOperatorPubkeys(e,r);return r&&this.progressBar.stop(),s}catch(s){throw r&&this.progressBar.stop(),new Error(s)}}async _getOperatorPubkeys(e,r){let{contractAddress:s,abi:t,genesisBlock:o}=S(this.params.network),n=new v.ethers.JsonRpcProvider(this.params.nodeUrl),d=new v.ethers.Contract(s,t,n),g=new v.ethers.Interface(t),i;try{i=await n.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=o;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||U.join(__dirname,"../../data");B.existsSync(k)||B.mkdirSync(k,{recursive:!0});let A=new Array(p.length),u=U.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 N="";try{N=v.AbiCoder.defaultAbiCoder().decode(["string"],E[2]).join("")}catch{N=E[2]}A[m]={id:m+1,pubkey:N}}return B.writeFileSync(u,JSON.stringify(A,null,2)),r&&this.progressBar.update(i,i),u}};var j=class extends f{constructor(){super("operator","Handles cluster operations")}setArguments(e){e.add_argument("-nw","--network",{help:"The network",choices:["mainnet","holesky","hoodi"],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),s=e.outputPath,t=await r.run(s,!0);console.log(`
Operator data has been saved to:
${t}`)}catch(r){console.error("\x1B[31m",r.message)}}};var se=async a=>new Promise(e=>{(0,Y.default)(a,(r,s)=>{if(r)return e("");e(s)})});async function L(){let a=`SSV Scanner v${D.version}`,e=await se(a);if(e){console.log(" -----------------------------------------------------------------------------------"),console.log(`${e||a}`),console.log(" -----------------------------------------------------------------------------------");for(let p of String(D.description).match(/.{1,75}/g)||[])console.log(` ${p}`);console.log(` -----------------------------------------------------------------------------------
`)}let r=new R.ArgumentParser,s=r.add_subparsers({title:"commands",dest:"command"}),t=new I,o=new _,n=new j,d=s.add_parser(t.name,{add_help:!0}),g=s.add_parser(o.name,{add_help:!0}),i=s.add_parser(n.name,{add_help:!0}),c="",l=q.argv.slice(2);switch(l[1]&&l[1].includes("--help")?(t.setArguments(d),o.setArguments(g),n.setArguments(i),r.parse_args()):(c=r.parse_known_args()[0].command,t.setArguments(d),o.setArguments(g),n.setArguments(i)),c){case t.name:await t.run(t.parse(l));break;case o.name:await o.run(o.parse(l));break;case n.name:await n.run(n.parse(l));break;default:console.error("Command not found"),q.exit(1)}}L();