Skip to content

Commit

Permalink
add proper func comments
Browse files Browse the repository at this point in the history
  • Loading branch information
ngmachado committed Aug 31, 2024
1 parent aeb7673 commit 6f14a9e
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 20 deletions.
3 changes: 2 additions & 1 deletion grt.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
#!/usr/bin/env bun

import path from "path";
import dotenv from "dotenv";

import yargs from "yargs"
import { hideBin } from "yargs/helpers"
import Graphinator from './src/graphinator';


const log = (msg: string, lineDecorator="") => console.log(`${new Date().toISOString()} - ${lineDecorator} (Graphinator) ${msg}`);


dotenv.config();

const argv = await yargs(hideBin(process.argv))
Expand Down
62 changes: 55 additions & 7 deletions src/datafetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ class DataFetcher {
private subgraphUrl: string;
private provider: JsonRpcProvider;

/**
* Creates an instance of DataFetcher.
* @param subgraphUrl - The URL of the subgraph endpoint.
* @param provider - The JSON RPC provider.
*/
constructor(subgraphUrl: string, provider: JsonRpcProvider) {
if(!subgraphUrl) {
throw new Error("subgraph URL not set");
Expand All @@ -22,7 +27,13 @@ class DataFetcher {
this.provider = provider;
}

// @TODO: refactor, we are mixing two concepts here get critical accounts with subgraph data
/**
* Fetches flows to liquidate based on the given token, GDA forwarder contract, and deposit consumed percentage threshold.
* @param token - The address of the token.
* @param gdaForwarder - The GDA forwarder contract.
* @param depositConsumedPctThreshold - The deposit consumed percentage threshold.
* @returns A promise that resolves to an array of Flow objects.
*/
async getFlowsToLiquidate(token: AddressLike, gdaForwarder: Contract, depositConsumedPctThreshold: number): Promise<Flow[]> {

const returnData: Flow[] = [];
Expand All @@ -31,7 +42,7 @@ class DataFetcher {

if (criticalAccounts.length > 0) {
for (const account of criticalAccounts) {
console.log("? Probing ", account.account.id, "token", account.token.id, "net fr", account.totalNetFlowRate, "cfa net fr", account.totalCFANetFlowRate, "gda net fr", await gdaForwarder.getNetFlow(account.token.id, account.account.id));
log(`? Probing ${account.account.id} token ${account.token.id} net fr ${account.totalNetFlowRate} cfa net fr ${account.totalCFANetFlowRate} gda net fr ${await gdaForwarder.getNetFlow(account.token.id, account.account.id)}`);
const rtb = await targetToken.realtimeBalanceOfNow(account.account.id);
const { availableBalance, deposit } = rtb;
if (availableBalance < 0) { // critical or insolvent
Expand All @@ -46,7 +57,7 @@ class DataFetcher {
if (netFlowRate >= ZERO) {
continue;
}
console.log("! Critical", account.account.id, "token", account.token.id, "net fr", netFlowRate, "(cfa", cfaNetFlowRate, "gda", gdaNetFlowRate, ")");
log(`! Critical ${account.account.id} token ${account.token.id} net fr ${netFlowRate} (cfa ${cfaNetFlowRate} gda ${gdaNetFlowRate})`);

const cfaFlows = await this.getOutgoingFlowsFromAccountByToken(account.token.id, account.account.id);
let processedCFAFlows = 0;
Expand Down Expand Up @@ -85,18 +96,24 @@ class DataFetcher {
}
}

console.log(` available balance ${availableBalance}, deposit ${deposit}, consumed deposit ${consumedDepositPercentage}%, flows to-be-liquidated/total: ${processedCFAFlows}/${cfaFlows.length} cfa | ${processedGDAFlows}/${gdaFlows.length} gda`);
log(`available balance ${availableBalance}, deposit ${deposit}, consumed deposit ${consumedDepositPercentage}%, flows to-be-liquidated/total: ${processedCFAFlows}/${cfaFlows.length} cfa | ${processedGDAFlows}/${gdaFlows.length} gda`);
if (processedCFAFlows > 0 || processedGDAFlows > 0) {
continue;
} else {
console.log("!!! no cfa|gda outflows to liquidate");
log("!!! no cfa|gda outflows to liquidate");
}
}
}
}
return returnData.sort((a, b) => Number(b.flowrate - a.flowrate));
}

/**
* Fetches outgoing flows from an account by token.
* @param token - The address of the token.
* @param account - The address of the account.
* @returns A promise that resolves to an array of outgoing flows.
*/
async getOutgoingFlowsFromAccountByToken(token: AddressLike, account: AddressLike): Promise<any[]> {
const _accountLowerCase = account.toString().toLowerCase();
const _tokenLowerCase = token.toString().toLowerCase();
Expand All @@ -121,6 +138,12 @@ class DataFetcher {
);
}

/**
* Fetches all flow distributions for a given token and account.
* @param token - The address of the token.
* @param account - The address of the account.
* @returns A promise that resolves to an array of flow distributions.
*/
async getAllFlowDistributions(token: AddressLike, account: AddressLike): Promise<any[]> {
const _accountLowerCase = account.toString().toLowerCase();
const _tokenLowerCase = token.toString().toLowerCase();
Expand All @@ -139,6 +162,11 @@ class DataFetcher {
);
}

/**
* Fetches critical accounts by token at the current time.
* @param token - The address of the token.
* @returns A promise that resolves to an array of critical accounts.
*/
async getCriticalAccountsByTokenNow(token: AddressLike): Promise<CriticalAccount[]> {
const _tokenLowerCase = token.toString().toLowerCase();
const timestamp = Math.floor(Date.now() / 1000);
Expand Down Expand Up @@ -169,6 +197,11 @@ class DataFetcher {
);
}

/**
* Fetches critical accounts at a specific timestamp.
* @param timestamp - The timestamp to fetch critical accounts at.
* @returns A promise that resolves to an array of critical accounts.
*/
async getCriticalAccountsAt(timestamp: number): Promise<CriticalAccount[]> {
return this._queryAllPages(
(lastId: string) => `{
Expand All @@ -195,7 +228,11 @@ class DataFetcher {
i => i
);
}

/**
* Fetches all super tokens.
* @param isListed - Whether to fetch listed tokens or not.
* @returns A promise that resolves to an array of super tokens.
*/
async getSuperTokens(isListed: boolean = true): Promise<any[]> {
return this._queryAllPages(
(lastId: string) => `{
Expand All @@ -213,7 +250,11 @@ class DataFetcher {
);
}

/// DataFetcher methods
/**
* Executes a GraphQL query.
* @param query - The GraphQL query string.
* @returns A promise that resolves to the Axios response.
*/
private async _graphql(query: string): Promise<AxiosResponse<any>> {

if (!this.subgraphUrl) {
Expand All @@ -228,6 +269,13 @@ class DataFetcher {
return await axios.post(this.subgraphUrl, {query}, {headers});
}

/**
* Queries all pages of a paginated GraphQL response.
* @param queryFn - A function that generates the GraphQL query string.
* @param toItems - A function that extracts items from the Axios response.
* @param itemFn - A function that processes each item.
* @returns A promise that resolves to an array of all items.
*/
private async _queryAllPages(queryFn: (lastId: string) => string, toItems: (res: AxiosResponse<any>) => any[], itemFn: (item: any) => any): Promise<any[]> {
let lastId = "";
const items: any[] = [];
Expand Down
67 changes: 55 additions & 12 deletions src/graphinator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ import {type AddressLike, ethers, type TransactionLike} from "ethers";
import DataFetcher from "./datafetcher.ts";
import type {Flow} from "./types/types.ts";
import sfMeta from "@superfluid-finance/metadata";

const BatchLiquidatorAbi = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/BatchLiquidator.sol/BatchLiquidator.json").abi;
const GDAv1ForwarderAbi = require("@superfluid-finance/ethereum-contracts/build/hardhat/contracts/utils/GDAv1Forwarder.sol/GDAv1Forwarder.json").abi;

const bigIntToStr = (key: string, value: any) => (typeof value === 'bigint' ? value.toString() : value);

const log = (msg: string, lineDecorator="") => console.log(`${new Date().toISOString()} - ${lineDecorator} (Graphinator) ${msg}`);
/**
* Graphinator is responsible for processing and liquidating flows.
*/
export default class Graphinator {

private dataFetcher: DataFetcher;
Expand All @@ -20,11 +22,18 @@ export default class Graphinator {
private gasMultiplier: number;
private maxGasPrice: number;

/**
* Creates an instance of Graphinator.
* @param networkName - The name of the network.
* @param batchSize - The size of the batch for processing flows.
* @param gasMultiplier - The gas multiplier for estimating gas limits.
* @param maxGasPrice - The maximum gas price allowed.
*/
constructor(networkName: string, batchSize: number, gasMultiplier: number, maxGasPrice: number) {
this.batchSize = batchSize;
this.gasMultiplier = gasMultiplier;
this.maxGasPrice = maxGasPrice;
console.log(`maxGasPrice: ${maxGasPrice} (${maxGasPrice / 1000000000} gwei)`);
log(`maxGasPrice: ${maxGasPrice} (${maxGasPrice / 1000000000} gwei)`);

const network = sfMeta.getNetworkByName(networkName);
if (network === undefined) {
Expand All @@ -41,43 +50,52 @@ export default class Graphinator {
if (!network.contractsV1.gdaV1Forwarder) {
throw new Error("GDA Forwarder contract address not found in metadata");
}
console.log(`(Graphinator) Initialized wallet: ${this.wallet.address}`);
log(`Initialized wallet: ${this.wallet.address}`);

this.gdaForwarder = new ethers.Contract(network.contractsV1.gdaV1Forwarder!, GDAv1ForwarderAbi, this.wallet);
if (!network.contractsV1.gdaV1Forwarder) {
throw new Error("Batch Liquidator contract address not found in metadata");
}
this.batchLiquidator = new ethers.Contract(network.contractsV1.batchLiquidator!, BatchLiquidatorAbi, this.wallet);
console.log(`(Graphinator) Initialized batch contract at ${network.contractsV1.batchLiquidator}`);
log(`Initialized batch contract at ${network.contractsV1.batchLiquidator}`);

this.depositConsumedPctThreshold = import.meta.env.DEPOSIT_CONSUMED_PCT_THRESHOLD
? Number(import.meta.env.DEPOSIT_CONSUMED_PCT_THRESHOLD)
: 20;
console.log(`(Graphinator) Will liquidate outflows of accounts with more than ${this.depositConsumedPctThreshold}% of the deposit consumed`);
log(`Will liquidate outflows of accounts with more than ${this.depositConsumedPctThreshold}% of the deposit consumed`);
}

// If no token is provided: first get a list of all tokens.
// Then for the provided or all tokens:
// get the outgoing flows of all critical accounts, then chunk and batch-liquidate them
/**
* Processes all tokens or a specific token to find and liquidate flows.
* @param token - The address of the token to process. If not provided, all tokens will be processed.
*/
async processAll(token?: AddressLike): Promise<void> {
const tokenAddrs = token ? [token] : await this._getSuperTokens();
for (const tokenAddr of tokenAddrs) {
const flowsToLiquidate = await this.dataFetcher.getFlowsToLiquidate(tokenAddr, this.gdaForwarder, this.depositConsumedPctThreshold);
if (flowsToLiquidate.length > 0) {
console.log(`Found ${flowsToLiquidate.length} flows to liquidate`);
log(`Found ${flowsToLiquidate.length} flows to liquidate`);
const chunks = this._chunkArray(flowsToLiquidate, this.batchSize);
for (const chunk of chunks) {
await this.batchLiquidateFlows(tokenAddr, chunk);
}
} else {
console.log(`(Graphinator) No critical accounts for token: ${tokenAddr}`);
log(`No critical accounts for token: ${tokenAddr}`);
}
}
}

// Liquidate all flows in one batch transaction.
// The caller is responsible for sizing the array such that it fits into one transaction.
// (Note: max digestible size depends on chain and context like account status, SuperApp receiver etc.)
/**
* Liquidates all flows in one batch transaction.
* @param token - The address of the token.
* @param flows - The array of flows to liquidate.
*/
private async batchLiquidateFlows(token: AddressLike, flows: Flow[]): Promise<void> {
try {
const txData = await this._generateBatchLiquidationTxData(token, flows);
Expand All @@ -95,27 +113,37 @@ export default class Graphinator {
};

if (process.env.DRY_RUN) {
console.log(`(Graphinator) Dry run - tx: ${JSON.stringify(tx, bigIntToStr)}`);
log(`Dry run - tx: ${JSON.stringify(tx, bigIntToStr)}`);
} else {
const signedTx = await this.wallet.signTransaction(tx);
const transactionResponse = await this.provider.broadcastTransaction(signedTx);
const receipt = await transactionResponse.wait();
console.log(`(Graphinator) Transaction successful: ${receipt?.hash}`);
log(`Transaction successful: ${receipt?.hash}`);
}
} else {
console.log(`(Graphinator) Gas price ${initialGasPrice} too high, skipping transaction`);
log(`Gas price ${initialGasPrice} too high, skipping transaction`);
await this._sleep(1000);
}
} catch (error) {
console.error(`(Graphinator) Error processing chunk: ${error}`);
}
}

/**
* Fetches all super tokens.
* @returns A promise that resolves to an array of super token addresses.
*/
private async _getSuperTokens(): Promise<AddressLike[]> {
return (await this.dataFetcher.getSuperTokens())
.map(token => token.id);
}

/**
* Splits an array into chunks of a specified size.
* @param array - The array to split.
* @param size - The size of each chunk.
* @returns An array of chunks.
*/
private _chunkArray<T>(array: T[], size: number): T[][] {
const chunks: T[][] = [];
for (let i = 0; i < array.length; i += size) {
Expand All @@ -124,7 +152,12 @@ export default class Graphinator {
return chunks;
}


/**
* Generates the transaction data for batch liquidation.
* @param token - The address of the token.
* @param flows - The array of flows to liquidate.
* @returns A promise that resolves to the transaction data.
*/
private async _generateBatchLiquidationTxData(token: AddressLike, flows: Flow[]): Promise<TransactionLike> {
if (!flows.every(flow => flow.token === token)) {
throw new Error("flow with wrong token");
Expand All @@ -139,6 +172,11 @@ export default class Graphinator {
return { data: transactionData, to: transactionTo };
}

/**
* Estimates the gas limit for a transaction.
* @param transaction - The transaction to estimate the gas limit for.
* @returns A promise that resolves to the estimated gas limit.
*/
private async _estimateGasLimit(transaction: TransactionLike): Promise<number> {
const gasEstimate = await this.provider.estimateGas({
to: transaction.to,
Expand All @@ -147,6 +185,11 @@ export default class Graphinator {
return Math.floor(Number(gasEstimate) * this.gasMultiplier);
}

/**
* Pauses execution for a specified amount of time.
* @param ms - The number of milliseconds to pause.
* @returns A promise that resolves after the specified time.
*/
private async _sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Expand Down

0 comments on commit 6f14a9e

Please sign in to comment.