-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
2,596 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
{ | ||
"name": "xfai", | ||
"version": "1.0.0", | ||
"description": "", | ||
"main": "index.js", | ||
"scripts": { | ||
"test": "echo \"Error: no test specified\" && exit 1", | ||
"start": "../dist/xfai/src/index.js", | ||
"compile": "tsc", | ||
"watch": "tsc -w", | ||
"clear": "rm -rf dist" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "ISC", | ||
"dependencies": { | ||
"@ethersproject/providers": "^5.7.2", | ||
"@types/big.js": "^6.2.2", | ||
"@types/lodash": "^4.17.0", | ||
"@types/pg": "^8.11.5", | ||
"big.js": "^6.2.1", | ||
"bignumber.js": "^9.1.2", | ||
"csv-parser": "^3.0.0", | ||
"decimal.js-light": "^2.5.1", | ||
"ethers": "^5.7.2", | ||
"fast-csv": "^5.0.1", | ||
"jsbi": "^4.3.0", | ||
"lodash": "^4.17.21", | ||
"pg": "^8.11.5", | ||
"tiny-invariant": "^1.3.1", | ||
"toformat": "^2.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^20.11.17", | ||
"typescript": "^5.3.3" | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
export const CHAIN_ID = 59144; | ||
export const RPC_URL = "https://rpc.linea.build"; | ||
export const LIQUIDITY_EVENTS_DB = | ||
"postgres://mobula_readonly:[email protected]:5432/xfai-logs?sslmode=no-verify"; | ||
export const XFAI_FACTORY = "0xa5136eAd459F0E61C99Cec70fe8F5C24cF3ecA26"; | ||
export const XFAI_POOL_INIT = | ||
"0xd29425d309539268aa2f934062f86ea332822e787dafc6baba7cfda029630330"; | ||
export const MULTICALL = "0xca11bde05977b3631167028862be2a173976ca11"; | ||
export const WETH = "0xe5D7C2a44FfDDf6b295A15c148167daaAf5Cf34f"; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,188 @@ | ||
import { Client } from "pg"; | ||
import { | ||
CHAIN_ID, | ||
LIQUIDITY_EVENTS_DB, | ||
RPC_URL, | ||
WETH, | ||
XFAI_FACTORY, | ||
XFAI_POOL_INIT, | ||
} from "./config"; | ||
import { keccak256, pack } from "@ethersproject/solidity"; | ||
import { uniq } from "lodash"; | ||
import { multicall } from "./sdk/mutlicall"; | ||
import { IXfaiPool__factory } from "./sdk/factories/IXfaiPool__factory"; | ||
import { getCreate2Address } from "ethers/lib/utils"; | ||
import { Block, StaticJsonRpcProvider } from "@ethersproject/providers"; | ||
import { format, write } from "@fast-csv/format"; | ||
import { time } from "console"; | ||
import { createWriteStream } from "fs"; | ||
function getPoolAddressFromTokenAddress(tokenAddress: string): string { | ||
return getCreate2Address( | ||
XFAI_FACTORY, | ||
keccak256(["bytes"], [pack(["address"], [tokenAddress])]), | ||
XFAI_POOL_INIT | ||
); | ||
} | ||
|
||
interface BlockData { | ||
blockNumber: number; | ||
blockTimestamp: number; | ||
} | ||
|
||
type OutputDataSchemaRow = { | ||
block_number: number; | ||
timestamp: number; | ||
user_address: string; | ||
token_address: string; | ||
token_balance: bigint; | ||
token_symbol: string; //token symbol should be empty string if it is not available | ||
usd_price: number; //assign 0 if not available | ||
}; | ||
|
||
async function getDBConnection() { | ||
const client = new Client(LIQUIDITY_EVENTS_DB); | ||
await client.connect(); | ||
return client; | ||
} | ||
|
||
async function getProvider() { | ||
const provider = new StaticJsonRpcProvider(RPC_URL, CHAIN_ID); | ||
await provider.ready; | ||
return provider; | ||
} | ||
|
||
type ChangedLiquidity = { | ||
owner: string; | ||
token: string; | ||
liquidity: number; | ||
timestamp: number; | ||
blockNumber: bigint; | ||
}; | ||
|
||
async function getUserTVLByBlock( | ||
block: BlockData | ||
): Promise<OutputDataSchemaRow[]> { | ||
const client = await getDBConnection(); | ||
const provider = await getProvider(); | ||
|
||
const liquidities = await client.query<ChangedLiquidity>({ | ||
text: ` | ||
SELECT owner, | ||
token, | ||
max("blockNumber") as "blockNumber", | ||
(max("date") / 1000) as "timestamp", | ||
sum(liquidity) as liquidity | ||
FROM "LiquidityTrace" | ||
WHERE "blockNumber" <= $1 | ||
AND LOWER("token") != LOWER($2) | ||
GROUP BY "owner", "token" | ||
HAVING sum(liquidity) > 0;`, | ||
values: [block.blockNumber, WETH], | ||
}); | ||
const pgSqlShutdown = client.end(); | ||
|
||
const liquiditiesRows = liquidities.rows.map((r) => ({ | ||
...r, | ||
pool: getPoolAddressFromTokenAddress(r.token), | ||
liquidity: BigInt(r.liquidity), | ||
})); | ||
|
||
const pools: string[] = uniq(liquiditiesRows.map(({ pool }) => pool)); | ||
|
||
const poolReserves = await multicall( | ||
provider, | ||
IXfaiPool__factory, | ||
pools.map((p) => ({ | ||
arguments: [], | ||
contractAddress: p, | ||
function_name: "getStates", | ||
})), | ||
{ | ||
allowFailure: false, | ||
callOverrides: { | ||
blockTag: block.blockNumber, | ||
}, | ||
} | ||
); | ||
const poolSupplies = await multicall( | ||
provider, | ||
IXfaiPool__factory, | ||
pools.map((p) => ({ | ||
arguments: [], | ||
contractAddress: p, | ||
function_name: "totalSupply", | ||
})), | ||
{ | ||
allowFailure: false, | ||
callOverrides: { | ||
blockTag: block.blockNumber, | ||
}, | ||
} | ||
); | ||
|
||
const poolRes = Object.fromEntries( | ||
Object.entries(poolReserves).map(([pool, [reserve, ethReserve]]) => [ | ||
pool, | ||
{ | ||
reserve, | ||
ethReserve, | ||
}, | ||
]) | ||
); | ||
|
||
const result: OutputDataSchemaRow[] = liquiditiesRows.flatMap( | ||
({ | ||
owner, | ||
token, | ||
pool: poolAddress, | ||
liquidity, | ||
blockNumber: block_number, | ||
timestamp, | ||
}) => { | ||
const poolSupply = poolSupplies[poolAddress]; | ||
const poolReserve = poolRes[poolAddress]; | ||
const tokenBalance = | ||
(liquidity * poolReserve.reserve.toBigInt()) / poolSupply.toBigInt(); | ||
const ethBalance = | ||
(liquidity * poolReserve.ethReserve.toBigInt()) / poolSupply.toBigInt(); | ||
return [ | ||
// Token reserve | ||
{ | ||
block_number: Number(block_number), | ||
timestamp, | ||
user_address: owner, | ||
token_address: token, | ||
token_balance: tokenBalance, | ||
token_symbol: "", | ||
usd_price: 0, | ||
}, | ||
// WETH Reserve | ||
{ | ||
block_number: Number(block_number), | ||
timestamp, | ||
user_address: owner, | ||
token_address: WETH, | ||
token_balance: ethBalance, | ||
token_symbol: "WETH", | ||
usd_price: 0, | ||
}, | ||
]; | ||
} | ||
); | ||
await Promise.all([pgSqlShutdown]); | ||
|
||
return result; | ||
} | ||
|
||
const ws = createWriteStream("outputData.csv"); | ||
|
||
getUserTVLByBlock({ blockNumber: 1140957, blockTimestamp: 1140957 }) | ||
.then((r) => { | ||
write(r, { headers: true }) | ||
.pipe(ws) | ||
.on("finish", () => { | ||
ws.close(); | ||
console.log("CSV file has been written."); | ||
}); | ||
}) | ||
.catch((e) => console.error(e)); |
Oops, something went wrong.