Last active
November 19, 2021 19:08
-
-
Save Reinis-FRP/a6d77e8e432924d33ef1fb93548d4ea6 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require('dotenv').config(); | |
const Web3 = require('web3'); | |
const web3 = new Web3(process.env.NODE_URL_CHAIN_1); | |
const web3Polygon = new Web3(process.env.NODE_URL_CHAIN_137); | |
const {hexToUtf8, toBN, toWei, fromWei} = web3.utils; | |
const fetch = require('node-fetch'); | |
const abiDecoder = require('abi-decoder'); | |
const governorAddress = '0x592349F7DeDB2b75f9d4F194d4b7C16D82E507Dc'; | |
const governorAbi = [ | |
{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint256","name":"id","type":"uint256"},{"components":[{"internalType":"address","name":"to","type":"address"},{"internalType":"uint256","name":"value","type":"uint256"},{"internalType":"bytes","name":"data","type":"bytes"}],"indexed":false,"internalType":"struct Governor.Transaction[]","name":"transactions","type":"tuple[]"}],"name":"NewProposal","type":"event"}, | |
]; | |
const governorRootTunnelAbi = [ | |
{"inputs":[{"internalType":"address","name":"to","type":"address"},{"internalType":"bytes","name":"data","type":"bytes"}],"name":"relayGovernance","outputs":[],"stateMutability":"nonpayable","type":"function"}, | |
]; | |
abiDecoder.addABI(governorRootTunnelAbi); | |
const tokenAbi = [ | |
{"inputs":[],"name":"decimals","outputs":[{"internalType":"uint8","name":"","type":"uint8"}],"stateMutability":"view","type":"function"}, | |
{"inputs":[],"name":"symbol","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}, | |
]; | |
// Some tokens (e.g. MKR) return symbol as bytes: | |
const altTokenAbi = [ | |
{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"bytes32"}],"payable":false,"stateMutability":"view","type":"function"}, | |
]; | |
const lookback = 3600 * 24; // Check availability of CoinGecko prices during the last 24h | |
const waitRateLimit = 60000; // If CoinGecko rate limits the response wait 60s before making the next request | |
let rateLimitedTill = 0; // Do not make CoinGecko request before this timestamp (need to store this globally due to multiple async requests) | |
// Wait timeout in miliseconds | |
const delay = ms => new Promise(res => setTimeout(res, ms)); | |
// Get token symbol from passed contract and web3 objects | |
async function getTokenSymbol(contract, web3) { | |
let symbol; | |
// First try string type, but if it fails fallback to alternative ABI encoded as bytes | |
try { | |
symbol = await contract.methods.symbol().call(); | |
} catch(e) { | |
const altContract = new web3.eth.Contract(altTokenAbi, contract.options.address); | |
symbol = hexToUtf8(await altContract.methods.symbol().call()); | |
} | |
return symbol; | |
} | |
// Convert to human readable token amount given raw hex value and token decimals | |
function scaleDownDecimals(hexAmount, decimals) { | |
return fromWei(toBN(hexAmount).mul(toBN(toWei('1'))).div(toBN('10').pow(toBN(decimals)))); | |
} | |
// Fetch CoinGecko price from token address | |
async function getCoingeckoPrice(platform, address, ccy, from, to) { | |
const url = 'https://api.coingecko.com/api/v3/coins/' + platform + '/contract/' + address + '/market_chart/range?vs_currency=' + ccy + '&from=' + from + '&to=' + to; | |
const response = await fetch(url); | |
if (response.status != 200) { | |
return [null, response.status]; | |
} | |
const json = await response.json(); | |
const prices = json.prices; | |
const lastPrice = prices.length ? prices.slice(-1)[0][1] : null; | |
return [lastPrice, response.status]; | |
} | |
// Wrap getCoingeckoPrice function to try repeatedly if blocked due to rate limiting | |
async function getRateLimitedPrice(platform, address, ccy, from, to) { | |
let lastPrice | |
let status; | |
while (true) { | |
const currentTime = Date.now(); | |
if (currentTime < rateLimitedTill) { | |
await delay(rateLimitedTill - currentTime); | |
} | |
[lastPrice, status] = await getCoingeckoPrice(platform, address, ccy, from, to); | |
if (status == 429) { | |
rateLimitedTill = Date.now() + waitRateLimit; | |
} else { | |
break; | |
} | |
} | |
return lastPrice; | |
} | |
// Decode proposed fee from event data section and add to the passed proposedFees object | |
function decodeFeeProposal(proposedFees, data) { | |
if (data.substr(0, 10) == web3.utils.sha3('setFinalFee(address,(uint256))').substr(0, 10)) { | |
const collateral = '0x' + data.substr(34, 40); | |
const rawFee = '0x' + data.substr(74, 64); | |
// If the collateral fee is changed it will be updated by the newest proposal since events are processed in order | |
proposedFees[collateral] = {rawFee: rawFee}; | |
return true; | |
} | |
return false; | |
} | |
// Add token symbol and scale down fee decimals | |
async function formatFees(proposedFees, web3) { | |
await Promise.all(Object.keys(proposedFees).map(async (collateral) => { | |
const contract = new web3.eth.Contract(tokenAbi, collateral); | |
const decimals = await contract.methods.decimals().call(); | |
const symbol = await getTokenSymbol(contract, web3); | |
const fee = scaleDownDecimals(proposedFees[collateral].rawFee, decimals); | |
proposedFees[collateral].fee = fee; | |
proposedFees[collateral].decimals = decimals; | |
proposedFees[collateral].symbol = symbol; | |
})); | |
} | |
// Wrap everything in an async function to allow the use of async/await. | |
(async () => { | |
const governor = new web3.eth.Contract(governorAbi, governorAddress); | |
const newProposals = await governor.getPastEvents('NewProposal', {fromBlock: 0}); | |
const proposedFeesEthereum = {}; | |
const proposedFeesPolygon = {}; | |
newProposals.forEach(proposal => { | |
const transactions = proposal.returnValues.transactions; | |
transactions.forEach(transaction => { | |
const data = transaction.data; | |
// First try to decode proposal as setFinalFee on Ethereum mainnet | |
const proposedOnEthereum = decodeFeeProposal(proposedFeesEthereum, data); | |
// If not an Ethereum proposal try to decode as relayGovernance for Polygon | |
if (!proposedOnEthereum && data.substr(0, 10) == web3.utils.sha3('relayGovernance(address,bytes)').substr(0, 10)) { | |
// Decode nested relayGovernance transaction | |
const decodedData = abiDecoder.decodeMethod(data); | |
if (decodedData && decodedData.params && decodedData.params[1] && decodedData.params[1].name == 'data' && decodedData.params[1].type == 'bytes') { | |
const nestedData = decodedData.params[1].value; | |
const proposedOnPolygon = decodeFeeProposal(proposedFeesPolygon, nestedData); | |
} | |
} | |
}); | |
}); | |
// Add token symbols and scale down fees | |
await formatFees(proposedFeesEthereum, web3); | |
await formatFees(proposedFeesPolygon, web3Polygon); | |
// Set time period for fetching CoinGecko prices | |
const toTime = parseInt(Date.now()/1000); | |
const fromTime = toTime - lookback; | |
// Try adding CoinGecko prices for Ethereum collateral tokens | |
await Promise.all(Object.keys(proposedFeesEthereum).map(async (collateral) => { | |
const price = await getRateLimitedPrice('ethereum', collateral, 'usd', fromTime, toTime); | |
if (price) proposedFeesEthereum[collateral].price = price; | |
})); | |
console.log('Ethereum mainnet (address, symbol, decimals, fee, price):'); | |
Object.keys(proposedFeesEthereum).forEach(collateral => { | |
if (proposedFeesEthereum[collateral].price) { | |
console.log(collateral, proposedFeesEthereum[collateral].symbol, proposedFeesEthereum[collateral].decimals, proposedFeesEthereum[collateral].fee, proposedFeesEthereum[collateral].price); | |
} else { | |
console.log(collateral, proposedFeesEthereum[collateral].symbol, proposedFeesEthereum[collateral].decimals, proposedFeesEthereum[collateral].fee); | |
} | |
}); | |
console.log('Polygon mainnet (address, symbol, decimals, fee):'); | |
Object.keys(proposedFeesPolygon).forEach(collateral => { | |
console.log(collateral, proposedFeesPolygon[collateral].symbol, proposedFeesPolygon[collateral].decimals, proposedFeesPolygon[collateral].fee); | |
}); | |
process.exit(0); | |
})().catch((e) => { | |
console.error(e); | |
process.exit(1); // Exit with a nonzero exit code to signal failure. | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment