From b39ff049754ece13a0394d561c39fa295891a8c9 Mon Sep 17 00:00:00 2001 From: ShubhamChndrvnshi Date: Mon, 30 Jan 2023 19:09:13 +0530 Subject: [PATCH 1/3] Query liquidations data from blockchain --- constants/legacyEmps.ts | 3 + containers/DevMining.ts | 7 +- containers/EmpSponsors.ts | 10 +-- containers/LiquidationsData.ts | 114 +++++++++++++++++++++++++++++++++ pages/_app.tsx | 41 ++++++------ utils/Contracts.ts | 11 +++- utils/events.ts | 51 +++++++++++++++ 7 files changed, 212 insertions(+), 25 deletions(-) create mode 100644 containers/LiquidationsData.ts create mode 100644 utils/events.ts diff --git a/constants/legacyEmps.ts b/constants/legacyEmps.ts index afe35c5..3033c5a 100644 --- a/constants/legacyEmps.ts +++ b/constants/legacyEmps.ts @@ -17,5 +17,8 @@ export const legacyEMPs: { [networkId: number]: string[] } = { "0xe478461458A6846279005c9416256E230376069f", "0xeb5c6dD4Ba0Bc93cbf9A5fDf8d6B1153a238f1CD", //"Perpetual", yUSD Kovan Oct30 "0x60d6c8E5Ca5D4A2aa1D0D4a4c72944A066008F75", + "0x7D1d0ad876168B0654391b8ADB17D1D157a29C41", + "0xC694Ce83BCf3f7f220BC9B62354CEC6A94446eBD", + "0x15Ff9a9831146642d55079deF442f7D304Ffa027", ], }; diff --git a/containers/DevMining.ts b/containers/DevMining.ts index c000cdc..f5e9492 100644 --- a/containers/DevMining.ts +++ b/containers/DevMining.ts @@ -11,7 +11,12 @@ import Connection from "./Connection"; export const defaultTotalRewards = 50000; const sumeroEMPs = { - empWhitelist: ["0xe478461458A6846279005c9416256E230376069f"], + empWhitelist: [ + "0xe478461458A6846279005c9416256E230376069f", + "0x7D1d0ad876168B0654391b8ADB17D1D157a29C41", + "0xC694Ce83BCf3f7f220BC9B62354CEC6A94446eBD", + "0x15Ff9a9831146642d55079deF442f7D304Ffa027", +], totalReward: 35000, }; diff --git a/containers/EmpSponsors.ts b/containers/EmpSponsors.ts index 3d1c3ae..ebbb0ce 100644 --- a/containers/EmpSponsors.ts +++ b/containers/EmpSponsors.ts @@ -1,6 +1,6 @@ import { createContainer } from "unstated-next"; import { useState, useEffect } from "react"; -import { utils } from "ethers"; +import { ethers, utils } from "ethers"; const fromWei = utils.formatUnits; import Connection from "./Connection"; @@ -14,6 +14,7 @@ import { isPricefeedInvertedFromTokenSymbol } from "../utils/getOffchainPrice"; import { useQuery } from "@apollo/client"; import { EMP_DATA } from "../apollo/uma/queries"; +import LiquidationsData from "./LiquidationsData"; // Interfaces for dApp state storage. interface SponsorPositionState { @@ -70,7 +71,8 @@ const useEmpSponsors = () => { const { latestPrice } = PriceFeed.useContainer(); const { decimals: collDecs } = Collateral.useContainer(); const { symbol: tokenSymbol } = Token.useContainer(); - + const { liquidations: liquidationsEventsData } = LiquidationsData.useContainer(); + const subgraphToQuery = `UMA${network?.chainId.toString()}`; const { loading, error } = useQuery(EMP_DATA, { context: { clientName: subgraphToQuery }, @@ -242,7 +244,7 @@ const useEmpSponsors = () => { } }); - empData.liquidations.forEach((liquidation) => { + liquidationsEventsData.forEach((liquidation) => { const liquidationCreatedEvent = liquidation.events.find( (e) => e.__typename === "LiquidationCreatedEvent" ); @@ -297,7 +299,7 @@ const useEmpSponsors = () => { // Change state when emp changes or when the graphQL data changes due to polling. useEffect(() => { querySponsors(); - }, [emp, latestPrice]); + }, [emp, latestPrice, liquidationsEventsData]); return { activeSponsors: activePositions, liquidations: liquidations }; }; diff --git a/containers/LiquidationsData.ts b/containers/LiquidationsData.ts new file mode 100644 index 0000000..8939542 --- /dev/null +++ b/containers/LiquidationsData.ts @@ -0,0 +1,114 @@ +import { createContainer } from "unstated-next"; +import { useState, useEffect } from "react"; +import EmpContract from "./EmpContract"; +import { getDisputeSettledEventData, getLiquidationCreatedEventData, getLiquidationDisputedEventData } from "../utils/events"; + +enum LiquidationStatus { + PreDispute = "PreDispute", + PendingDispute = "PendingDispute", + DisputeSucceeded = "DisputeSucceeded", + DisputeFailed = "DisputeFailed" +} + +type event = { + __typename: string; + timestamp: string; + tx_hash: string; +}; + +type LiquidationData = { + __typename: string; + id: string; + sponsor: { + __typename: string; + id: string; + }; + liquidationId: string; + liquidator: string; + disputer: string | null; + tokensLiquidated: string; + lockedCollateral: string; + liquidatedCollateral: string; + status: LiquidationStatus; + events: event[] +} + +function useLiquidationsDataGetter() { + const { contract: emp } = EmpContract.useContainer(); + + + const [liquidations, setLiquidations] = useState([]); + + const queryLiquidations = async () => { + if (!(emp && emp !== null)) return; + setLiquidations([]); + const rawLiquidationsData = await getLiquidationCreatedEventData(emp); + rawLiquidationsData.map(async (item: any) => { + + const filter = [item.args.sponsor, item.args.liquidator] + + const disputeData = await getLiquidationDisputedEventData(emp, filter) + + const events: event[] = []; + let status: LiquidationStatus = LiquidationStatus.PreDispute; + if (disputeData.length) { + status = LiquidationStatus.PendingDispute; + events.push({ + __typename: disputeData[0].event + "Event", + timestamp: (await disputeData[0].getBlock()).timestamp, + tx_hash: disputeData[0].transactionHash, + }) + + const disputeSettlementData = await getDisputeSettledEventData(emp, [null, ...filter]) + if (disputeSettlementData.length) { + events.push({ + __typename: disputeSettlementData[0].event + "Event", + timestamp: (await disputeSettlementData[0].getBlock()).timestamp, + tx_hash: disputeSettlementData[0].transactionHash, + }) + status = disputeSettlementData[0].args.disputeSucceeded ? LiquidationStatus.DisputeSucceeded : LiquidationStatus.DisputeFailed; + } + } + + setLiquidations((prevState: LiquidationData[]) => { + return [...prevState, { + __typename: "Liquidation", + id: item.args.sponsor + '-' + emp.address + '-' + item.args.liquidationId.toString(), + sponsor: { + __typename: "Sponsor", + id: item.args.sponsor + }, + liquidationId: item.args.liquidationId.toString(), + liquidator: item.args.liquidator, + disputer: null, + tokensLiquidated: item.args.tokensOutstanding.toString(), + lockedCollateral: item.args.lockedCollateral.toString(), + // Need to test this value + liquidatedCollateral: item.args.tokensOutstanding.toString(), + status: status, + events: [ + { + __typename: item.event + "Event", + timestamp: item.args.liquidationTime.toString(), + tx_hash: item.transactionHash, + }, + ...events + ] + }] + }) + }) + }; + + // update liquidations data depends on emp + useEffect(() => { + queryLiquidations() + }, [emp]); + + return { + liquidations, + }; +} + +const LiquidationsData = createContainer(useLiquidationsDataGetter); + +export default LiquidationsData; diff --git a/pages/_app.tsx b/pages/_app.tsx index cb01b9a..81280bd 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -24,6 +24,7 @@ import DevMining from "../containers/DevMining"; import { UniswapGetPair } from "../containers/Uniswap"; import ContractList from "../containers/ContractList"; import ContractState from "../containers/ContractState"; +import LiquidationsData from "../containers/LiquidationsData"; import { ApolloProvider } from "@apollo/client"; import { client } from "../apollo/client"; @@ -45,25 +46,27 @@ const WithStateContainerProviders = ({ children }: IProps) => ( - - - - - - - - - - {children} - - - - - - - - - + + + + + + + + + + + {children} + + + + + + + + + + diff --git a/utils/Contracts.ts b/utils/Contracts.ts index f44180d..43ed164 100644 --- a/utils/Contracts.ts +++ b/utils/Contracts.ts @@ -46,10 +46,19 @@ export const Contracts: { [networkId: number]: ContractArguments[] } = { ], 5: [ ["0xe478461458a6846279005c9416256e230376069f", "EMP", "3"], // sumero emp - // ["0x4Aad7B81dCc4f765E6F57510e34A1bD00aFCb316", "EMP", "1"], + ["0x7D1d0ad876168B0654391b8ADB17D1D157a29C41", "EMP", "3"], + ["0xC694Ce83BCf3f7f220BC9B62354CEC6A94446eBD", "EMP", "3"], + ["0x15Ff9a9831146642d55079deF442f7D304Ffa027", "EMP", "3"], ].reverse() as ContractArguments[], }; +export const contractToBlockCreated: { [key: string]: number; } = { + "0x7d1d0ad876168b0654391b8adb17d1d157a29c41": 8282059, + "0xc694ce83bcf3f7f220bc9b62354cec6a94446ebd": 8255278, + "0x15ff9a9831146642d55079def442f7d304ffa027": 8297886, + "0xe478461458a6846279005c9416256e230376069f": 8362562, +} + export const getByAddress = (address: string, network: number) => { assert(Contracts[network], "Invalid Network: " + network); const found = Contracts[network].find((info: ContractArguments) => { diff --git a/utils/events.ts b/utils/events.ts new file mode 100644 index 0000000..ff2d4b1 --- /dev/null +++ b/utils/events.ts @@ -0,0 +1,51 @@ +import { Contract } from "ethers"; +import { contractToBlockCreated } from "./Contracts"; + +export const getEventData = async (contract: Contract, eventName: string, filter: Array | [], startBlock?: number, endBlock?: number) => { + + if (!startBlock || !endBlock) return contract.queryFilter(contract.filters[eventName](...filter)); + + let allEvents: any[] = []; + let from = startBlock; + let to = endBlock; + // Since query filter sometime fails and only works for up untill 5000 blocks, this works if block diff > 5000 b/w startBlock and endBlock. + for (let i = from; i < to; i += 5000) { + const _startBlock = i; + const _endBlock = Math.min(to, i + 4999); + const events = await contract.queryFilter(contract.filters[eventName](...filter), _startBlock, _endBlock); + allEvents = [...allEvents, ...events] + } + return allEvents +} + +export const getLiquidationCreatedEventData = async (contract: Contract, filter: Array = []) => { + const start: number = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); + + return getEventData(contract, "LiquidationCreated", filter, start, end) +} + +export const getLiquidationDisputedEventData = async (contract: Contract, filter: Array = []) => { + const start = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); + + return getEventData(contract, "LiquidationDisputed", filter, start, end) +} + +export const getDisputeSettledEventData = async (contract: Contract, filter: Array = []) => { + const start = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); + + return getEventData(contract, "DisputeSettled", filter, start, end) +} + +export const getLiquidationWithdrawnEventData = async (contract: Contract, filter: Array = []) => { + const start = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); + + return getEventData(contract, "LiquidationWithdrawn", filter, start, end) +} + + + + From 5b85e0b57e25c671edf77e1502d1f997fbeda182 Mon Sep 17 00:00:00 2001 From: ShubhamChndrvnshi Date: Mon, 30 Jan 2023 19:39:32 +0530 Subject: [PATCH 2/3] added gold case for exchange symbol map and some comments. --- constants/syntheticTokenToExchangeMap.ts | 4 ++- containers/EmpSponsors.ts | 38 ++++++------------------ 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/constants/syntheticTokenToExchangeMap.ts b/constants/syntheticTokenToExchangeMap.ts index 8b478ab..e074925 100644 --- a/constants/syntheticTokenToExchangeMap.ts +++ b/constants/syntheticTokenToExchangeMap.ts @@ -49,7 +49,7 @@ export const EXCHANGE_LINK_MAP: ExchangeLinkMap = { // This returns a Balancer exchange URL to swap `tokenAddress` and sell USDC, a well understood currency }; /* -SUMERO FIX: case BTC added +SUMERO FIX: case BTC, Gold added */ // Returns a mapping from synthetic token to the type of exchange on which you can acquire the chosen token export const getExchangeTypeFromTokenSymbol = (symbol: string | null) => { @@ -76,6 +76,8 @@ export const getExchangeTypeFromTokenSymbol = (symbol: string | null) => { return TOKEN_TO_EXCHANGE_MAP.openeth; case symbol?.includes("BTC"): return TOKEN_TO_EXCHANGE_MAP.usdbtc; + case symbol?.includes("Gold"): + return TOKEN_TO_EXCHANGE_MAP.usdbtc; default: return null; } diff --git a/containers/EmpSponsors.ts b/containers/EmpSponsors.ts index ebbb0ce..8b434bb 100644 --- a/containers/EmpSponsors.ts +++ b/containers/EmpSponsors.ts @@ -71,8 +71,13 @@ const useEmpSponsors = () => { const { latestPrice } = PriceFeed.useContainer(); const { decimals: collDecs } = Collateral.useContainer(); const { symbol: tokenSymbol } = Token.useContainer(); - const { liquidations: liquidationsEventsData } = LiquidationsData.useContainer(); - + /* +SUMERO FIX: using events data instead of subgraphs for data query +*/ + const { + liquidations: liquidationsEventsData, + } = LiquidationsData.useContainer(); + const subgraphToQuery = `UMA${network?.chainId.toString()}`; const { loading, error } = useQuery(EMP_DATA, { context: { clientName: subgraphToQuery }, @@ -83,7 +88,7 @@ const useEmpSponsors = () => { financialContracts: [ { __typename: "FinancialContract", - id: "0xe478461458a6846279005c9416256e230376069f", + id: emp?.address, positions: [ { __typename: "SponsorPosition", @@ -125,32 +130,6 @@ const useEmpSponsors = () => { }, }, ], - liquidations: [ - { - __typename: "Liquidation", - id: - "0xa77ac35aa49536cd0539798eeb2fee1b72f64679-0xfa3aa7ee08399a4ce0b4921c85ab7d645ccac669-0", - sponsor: { - __typename: "Sponsor", - id: "0xa77ac35aa49536cd0539798eeb2fee1b72f64679", - }, - liquidationId: "0", - liquidator: "0x428ab2ba90eba0a4be7af34c9ac451ab061ac010", - disputer: null, - tokensLiquidated: "60", - lockedCollateral: "8.4002", - liquidatedCollateral: "8.4002", - status: "PreDispute", - events: [ - { - __typename: "LiquidationCreatedEvent", - timestamp: "1613843412", - tx_hash: - "0xcb23eb1c291980bee6218e72971d08d6ea70200802a6c449a9dfe64e92643055", - }, - ], - }, - ], }, ], }; @@ -289,6 +268,7 @@ const useEmpSponsors = () => { } } }); + console.log("newLiquidations", newLiquidations); setActivePositions(newPositions); setLiquidations(newLiquidations); } From 902d4ddd70f796b7df060e65109df52def21593a Mon Sep 17 00:00:00 2001 From: ShubhamChndrvnshi Date: Wed, 1 Feb 2023 09:55:21 +0530 Subject: [PATCH 3/3] sponsors and liquidation data from events --- containers/EmpSponsors.ts | 264 ++++++++++++++------------------- containers/LiquidationsData.ts | 205 +++++++++++++------------ containers/SponsorsData.ts | 90 +++++++++++ pages/_app.tsx | 45 +++--- utils/events.ts | 108 +++++++++----- utils/getAbi.ts | 2 +- 6 files changed, 409 insertions(+), 305 deletions(-) create mode 100644 containers/SponsorsData.ts diff --git a/containers/EmpSponsors.ts b/containers/EmpSponsors.ts index 8b434bb..31c4779 100644 --- a/containers/EmpSponsors.ts +++ b/containers/EmpSponsors.ts @@ -15,6 +15,7 @@ import { isPricefeedInvertedFromTokenSymbol } from "../utils/getOffchainPrice"; import { useQuery } from "@apollo/client"; import { EMP_DATA } from "../apollo/uma/queries"; import LiquidationsData from "./LiquidationsData"; +import SponsorsData from "./SponsorsData"; // Interfaces for dApp state storage. interface SponsorPositionState { @@ -76,63 +77,19 @@ SUMERO FIX: using events data instead of subgraphs for data query */ const { liquidations: liquidationsEventsData, + loading: isGettingLiquidationsData, } = LiquidationsData.useContainer(); + const { + positionsData, + loading: isGettingPositionsData, + } = SponsorsData.useContainer(); + const subgraphToQuery = `UMA${network?.chainId.toString()}`; const { loading, error } = useQuery(EMP_DATA, { context: { clientName: subgraphToQuery }, pollInterval: 10000, }); - // Data is hardcoded - const data = { - financialContracts: [ - { - __typename: "FinancialContract", - id: emp?.address, - positions: [ - { - __typename: "SponsorPosition", - collateral: "1.064204355139777562", - isEnded: false, - tokensOutstanding: "5", - withdrawalRequestPassTimestamp: "0", - withdrawalRequestAmount: "0", - transferPositionRequestPassTimestamp: "0", - sponsor: { - __typename: "Sponsor", - id: "0x53c91f33e4da805d04dce861c536fa1674e7334d", - }, - }, - { - __typename: "SponsorPosition", - collateral: "10", - isEnded: false, - tokensOutstanding: "33", - withdrawalRequestPassTimestamp: "0", - withdrawalRequestAmount: "0", - transferPositionRequestPassTimestamp: "0", - sponsor: { - __typename: "Sponsor", - id: "0x7eb9d67f9daea510399a1ee978b36e66626058d3", - }, - }, - { - __typename: "SponsorPosition", - collateral: "41.188539593994385011", - isEnded: false, - tokensOutstanding: "125.91889443999761803", - withdrawalRequestPassTimestamp: "0", - withdrawalRequestAmount: "0", - transferPositionRequestPassTimestamp: "0", - sponsor: { - __typename: "Sponsor", - id: "0xffb607418dbeab7a888e079a34be28a30d8e1de2", - }, - }, - ], - }, - ], - }; const getCollateralRatio = ( collateral: number, @@ -158,120 +115,119 @@ SUMERO FIX: using events data instead of subgraphs for data query latestPrice !== null && collateralRequirement !== null && collDecs !== null && - tokenSymbol !== null + tokenSymbol !== null && + !isGettingLiquidationsData && + !isGettingPositionsData ) { if (error) { console.error(`Apollo client failed to fetch graph data:`, error); } - if (!loading && data) { + if (!loading) { // The subgraph query doesn't have data of sumero emp contracts - const empData = data.financialContracts.find( - (contract: any) => - utils.getAddress(contract.id).toLowerCase() === - emp.address.toLowerCase() - ); - if (empData) { - let newPositions: SponsorMap = {}; - let newLiquidations: LiquidationMap = {}; - - const collReqFromWei = parseFloat(fromWei(collateralRequirement)); - - empData.positions.forEach((position: PositionQuery) => { - if (position.isEnded) return; - - const sponsor = utils.getAddress(position.sponsor.id); - const backingCollateral = - Number(position.collateral) - - Number(position.withdrawalRequestAmount); + // const empData = data.financialContracts.find( + // (contract: any) => + // utils.getAddress(contract.id).toLowerCase() === + // emp.address.toLowerCase() + // ); + // if (empData) { + let newPositions: SponsorMap = {}; + let newLiquidations: LiquidationMap = {}; + + const collReqFromWei = parseFloat(fromWei(collateralRequirement)); + + positionsData.forEach((position: PositionQuery) => { + if (position.isEnded) return; + + const sponsor = utils.getAddress(position.sponsor.id); + const backingCollateral = + Number(position.collateral) - + Number(position.withdrawalRequestAmount); + + const cRatio = getCollateralRatio( + backingCollateral, + Number(position.tokensOutstanding), + latestPrice + ); + const liquidationPrice = getLiquidationPrice( + backingCollateral, + Number(position.tokensOutstanding), + collReqFromWei, + isPricefeedInvertedFromTokenSymbol(tokenSymbol) + ); + const pendingWithdraw = + position.withdrawalRequestPassTimestamp === "0" ? "No" : "Yes"; + + const pendingTransfer = + position.transferPositionRequestPassTimestamp === "0" + ? "No" + : "Yes"; + if ( + position.tokensOutstanding !== "0" && + position.collateral !== "0" + ) { + newPositions[sponsor] = { + tokensOutstanding: position.tokensOutstanding, + collateral: position.collateral, + backingCollateral: backingCollateral.toString(), + cRatio: cRatio.toString(), + liquidationPrice: liquidationPrice.toString(), + pendingWithdraw: pendingWithdraw, + pendingTransfer: pendingTransfer, + withdrawalRequestAmount: position.withdrawalRequestAmount, + withdrawalTimestamp: position.withdrawalRequestPassTimestamp, + transferTimestamp: position.transferPositionRequestPassTimestamp, + sponsor, + }; + } + }); + + liquidationsEventsData.forEach((liquidation) => { + const liquidationCreatedEvent = liquidation.events.find( + (e) => e.__typename === "LiquidationCreatedEvent" + ); + + // There should always be a LiquidationCreatedEvent associated with each liquidation object, but if not then + // we will just ignore this strange edge case. + if (liquidationCreatedEvent) { + const liquidatedCR = + parseFloat(liquidation.tokensLiquidated) > 0 + ? parseFloat(liquidation.liquidatedCollateral) / + parseFloat(liquidation.tokensLiquidated) + : 0; + const maxDisputablePrice = + parseFloat(liquidation.tokensLiquidated) > 0 && collReqFromWei > 0 + ? parseFloat(liquidation.liquidatedCollateral) / + (parseFloat(liquidation.tokensLiquidated) * collReqFromWei) + : 0; - const cRatio = getCollateralRatio( - backingCollateral, - Number(position.tokensOutstanding), - latestPrice - ); - const liquidationPrice = getLiquidationPrice( - backingCollateral, - Number(position.tokensOutstanding), - collReqFromWei, - isPricefeedInvertedFromTokenSymbol(tokenSymbol) - ); - const pendingWithdraw = - position.withdrawalRequestPassTimestamp === "0" ? "No" : "Yes"; - - const pendingTransfer = - position.transferPositionRequestPassTimestamp === "0" - ? "No" - : "Yes"; if ( - position.tokensOutstanding !== "0" && - position.collateral !== "0" + liquidation.tokensLiquidated !== "0" && + liquidation.lockedCollateral !== "0" ) { - newPositions[sponsor] = { - tokensOutstanding: position.tokensOutstanding, - collateral: position.collateral, - backingCollateral: backingCollateral.toString(), - cRatio: cRatio.toString(), - liquidationPrice: liquidationPrice.toString(), - pendingWithdraw: pendingWithdraw, - pendingTransfer: pendingTransfer, - withdrawalRequestAmount: position.withdrawalRequestAmount, - withdrawalTimestamp: position.withdrawalRequestPassTimestamp, - transferTimestamp: - position.transferPositionRequestPassTimestamp, - sponsor, + // The UMA subgraph uniquely identifies each liquidation with an "id" that concatenates + // the liquidated sponsor's address with the liquidation ID, for example: + // "0x1e17a75616cd74f5846b1b71622aa8e10ea26cc0-0" + const sponsorAddressPlusId = liquidation.id; + newLiquidations[sponsorAddressPlusId] = { + sponsor: utils.getAddress(liquidation.sponsor.id), + liquidator: liquidation.liquidator, + disputer: String(liquidation.disputer), + liquidationId: liquidation.liquidationId, + liquidatedCR: liquidatedCR.toString(), + maxDisputablePrice: maxDisputablePrice.toString(), + tokensLiquidated: liquidation.tokensLiquidated, + lockedCollateral: liquidation.lockedCollateral, + liquidatedCollateral: liquidation.liquidatedCollateral, + status: liquidation.status, + liquidationTimestamp: liquidationCreatedEvent.timestamp, + liquidationReceipt: liquidationCreatedEvent.tx_hash, }; } - }); - - liquidationsEventsData.forEach((liquidation) => { - const liquidationCreatedEvent = liquidation.events.find( - (e) => e.__typename === "LiquidationCreatedEvent" - ); - - // There should always be a LiquidationCreatedEvent associated with each liquidation object, but if not then - // we will just ignore this strange edge case. - if (liquidationCreatedEvent) { - const liquidatedCR = - parseFloat(liquidation.tokensLiquidated) > 0 - ? parseFloat(liquidation.liquidatedCollateral) / - parseFloat(liquidation.tokensLiquidated) - : 0; - const maxDisputablePrice = - parseFloat(liquidation.tokensLiquidated) > 0 && - collReqFromWei > 0 - ? parseFloat(liquidation.liquidatedCollateral) / - (parseFloat(liquidation.tokensLiquidated) * collReqFromWei) - : 0; - - if ( - liquidation.tokensLiquidated !== "0" && - liquidation.lockedCollateral !== "0" - ) { - // The UMA subgraph uniquely identifies each liquidation with an "id" that concatenates - // the liquidated sponsor's address with the liquidation ID, for example: - // "0x1e17a75616cd74f5846b1b71622aa8e10ea26cc0-0" - const sponsorAddressPlusId = liquidation.id; - newLiquidations[sponsorAddressPlusId] = { - sponsor: utils.getAddress(liquidation.sponsor.id), - liquidator: liquidation.liquidator, - disputer: String(liquidation.disputer), - liquidationId: liquidation.liquidationId, - liquidatedCR: liquidatedCR.toString(), - maxDisputablePrice: maxDisputablePrice.toString(), - tokensLiquidated: liquidation.tokensLiquidated, - lockedCollateral: liquidation.lockedCollateral, - liquidatedCollateral: liquidation.liquidatedCollateral, - status: liquidation.status, - liquidationTimestamp: liquidationCreatedEvent.timestamp, - liquidationReceipt: liquidationCreatedEvent.tx_hash, - }; - } - } - }); - console.log("newLiquidations", newLiquidations); - setActivePositions(newPositions); - setLiquidations(newLiquidations); - } + } + }); + console.log("newLiquidations", newLiquidations); + setActivePositions(newPositions); + setLiquidations(newLiquidations); } } }; diff --git a/containers/LiquidationsData.ts b/containers/LiquidationsData.ts index 8939542..f18cf3b 100644 --- a/containers/LiquidationsData.ts +++ b/containers/LiquidationsData.ts @@ -1,112 +1,129 @@ import { createContainer } from "unstated-next"; import { useState, useEffect } from "react"; import EmpContract from "./EmpContract"; -import { getDisputeSettledEventData, getLiquidationCreatedEventData, getLiquidationDisputedEventData } from "../utils/events"; +import { + getDisputeSettledEventData, + getLiquidationCreatedEventData, + getLiquidationDisputedEventData, +} from "../utils/events"; enum LiquidationStatus { - PreDispute = "PreDispute", - PendingDispute = "PendingDispute", - DisputeSucceeded = "DisputeSucceeded", - DisputeFailed = "DisputeFailed" + PreDispute = "PreDispute", + PendingDispute = "PendingDispute", + DisputeSucceeded = "DisputeSucceeded", + DisputeFailed = "DisputeFailed", } type event = { - __typename: string; - timestamp: string; - tx_hash: string; + __typename: string; + timestamp: string; + tx_hash: string; }; type LiquidationData = { + __typename: string; + id: string; + sponsor: { __typename: string; id: string; - sponsor: { - __typename: string; - id: string; - }; - liquidationId: string; - liquidator: string; - disputer: string | null; - tokensLiquidated: string; - lockedCollateral: string; - liquidatedCollateral: string; - status: LiquidationStatus; - events: event[] -} + }; + liquidationId: string; + liquidator: string; + disputer: string | null; + tokensLiquidated: string; + lockedCollateral: string; + liquidatedCollateral: string; + status: LiquidationStatus; + events: event[]; +}; function useLiquidationsDataGetter() { - const { contract: emp } = EmpContract.useContainer(); - - - const [liquidations, setLiquidations] = useState([]); - - const queryLiquidations = async () => { - if (!(emp && emp !== null)) return; - setLiquidations([]); - const rawLiquidationsData = await getLiquidationCreatedEventData(emp); - rawLiquidationsData.map(async (item: any) => { - - const filter = [item.args.sponsor, item.args.liquidator] - - const disputeData = await getLiquidationDisputedEventData(emp, filter) - - const events: event[] = []; - let status: LiquidationStatus = LiquidationStatus.PreDispute; - if (disputeData.length) { - status = LiquidationStatus.PendingDispute; - events.push({ - __typename: disputeData[0].event + "Event", - timestamp: (await disputeData[0].getBlock()).timestamp, - tx_hash: disputeData[0].transactionHash, - }) - - const disputeSettlementData = await getDisputeSettledEventData(emp, [null, ...filter]) - if (disputeSettlementData.length) { - events.push({ - __typename: disputeSettlementData[0].event + "Event", - timestamp: (await disputeSettlementData[0].getBlock()).timestamp, - tx_hash: disputeSettlementData[0].transactionHash, - }) - status = disputeSettlementData[0].args.disputeSucceeded ? LiquidationStatus.DisputeSucceeded : LiquidationStatus.DisputeFailed; - } - } - - setLiquidations((prevState: LiquidationData[]) => { - return [...prevState, { - __typename: "Liquidation", - id: item.args.sponsor + '-' + emp.address + '-' + item.args.liquidationId.toString(), - sponsor: { - __typename: "Sponsor", - id: item.args.sponsor - }, - liquidationId: item.args.liquidationId.toString(), - liquidator: item.args.liquidator, - disputer: null, - tokensLiquidated: item.args.tokensOutstanding.toString(), - lockedCollateral: item.args.lockedCollateral.toString(), - // Need to test this value - liquidatedCollateral: item.args.tokensOutstanding.toString(), - status: status, - events: [ - { - __typename: item.event + "Event", - timestamp: item.args.liquidationTime.toString(), - tx_hash: item.transactionHash, - }, - ...events - ] - }] - }) - }) - }; - - // update liquidations data depends on emp - useEffect(() => { - queryLiquidations() - }, [emp]); - - return { - liquidations, - }; + const { contract: emp } = EmpContract.useContainer(); + + const [liquidations, setLiquidations] = useState([]); + const [loading, setLoading] = useState(true); + + const queryLiquidations = async () => { + if (!(emp && emp !== null)) return; + const rawLiquidationsData = await getLiquidationCreatedEventData(emp); + rawLiquidationsData.map(async (item: any) => { + const filter = [item.args.sponsor, item.args.liquidator]; + + const disputeData = await getLiquidationDisputedEventData(emp, filter); + + const disputeRaisedEventsData: event[] = []; + let status: LiquidationStatus = LiquidationStatus.PreDispute; + if (disputeData.length) { + status = LiquidationStatus.PendingDispute; + disputeRaisedEventsData.push({ + __typename: disputeData[0].event + "Event", + timestamp: (await disputeData[0].getBlock()).timestamp, + tx_hash: disputeData[0].transactionHash, + }); + + const disputeSettlementData = await getDisputeSettledEventData(emp, [ + null, + ...filter, + ]); + if (disputeSettlementData.length) { + disputeRaisedEventsData.push({ + __typename: disputeSettlementData[0].event + "Event", + timestamp: (await disputeSettlementData[0].getBlock()).timestamp, + tx_hash: disputeSettlementData[0].transactionHash, + }); + status = disputeSettlementData[0].args.disputeSucceeded + ? LiquidationStatus.DisputeSucceeded + : LiquidationStatus.DisputeFailed; + } + } + // Setting loading false as even with 1 liquidation data can be populated in UI and as the state updates data will also update in UI accordingly + loading ? setLoading(false) : {}; + setLiquidations((prevState: LiquidationData[]) => { + return [ + ...prevState, + { + __typename: "Liquidation", + id: + item.args.sponsor + + "-" + + emp.address + + "-" + + item.args.liquidationId.toString(), + sponsor: { + __typename: "Sponsor", + id: item.args.sponsor, + }, + liquidationId: item.args.liquidationId.toString(), + liquidator: item.args.liquidator, + disputer: null, + tokensLiquidated: item.args.tokensOutstanding.toString(), + lockedCollateral: item.args.lockedCollateral.toString(), + // Need to test this value + liquidatedCollateral: item.args.tokensOutstanding.toString(), + status: status, + events: [ + { + __typename: item.event + "Event", + timestamp: item.args.liquidationTime.toString(), + tx_hash: item.transactionHash, + }, + ...disputeRaisedEventsData, + ], + }, + ]; + }); + }); + }; + + // update liquidations data depends on emp + useEffect(() => { + queryLiquidations(); + }, [emp]); + + return { + liquidations, + loading, + }; } const LiquidationsData = createContainer(useLiquidationsDataGetter); diff --git a/containers/SponsorsData.ts b/containers/SponsorsData.ts new file mode 100644 index 0000000..5ce2f75 --- /dev/null +++ b/containers/SponsorsData.ts @@ -0,0 +1,90 @@ +import { createContainer } from "unstated-next"; +import { useState, useEffect } from "react"; +import EmpContract from "./EmpContract"; +import { + getEndedSponsorPositionEventData, + getPositionCreatedEventData, +} from "../utils/events"; + +type SponsorData = { + __typename: string; // SponsorPosition + collateral: string; + isEnded: boolean; + tokensOutstanding: string; + withdrawalRequestPassTimestamp: string; + withdrawalRequestAmount: string; + transferPositionRequestPassTimestamp: string; + sponsor: { + __typename: string; // Sponsor + id: string; + }; +}; + +function useSponsorsDataGetter() { + const { contract: emp } = EmpContract.useContainer(); + + const [positionsData, setPositionsData] = useState([]); + const [loading, setLoading] = useState(true); + + const querySponsorsData = async () => { + if (!(emp && emp !== null)) return; + const rawPositionEventData = await getPositionCreatedEventData(emp); + const allPositionsData = await Promise.all( + rawPositionEventData.map(async (item: any) => { + const filter = [item.args.sponsor]; + const result = { + pos: await emp.positions(item.args.sponsor), + isEnded: Boolean( + (await getEndedSponsorPositionEventData(emp, filter))[0] + ), + }; + return result; + }) + ); + + console.log({ + allPositionsData, + }); + + rawPositionEventData.map((item: any, index: number) => { + const newPos = { + __typename: "SponsorPosition", + collateral: allPositionsData[index].pos.collateral.toString(), + isEnded: allPositionsData[index].isEnded, + tokensOutstanding: item.args.tokenAmount.toString(), + withdrawalRequestPassTimestamp: allPositionsData[ + index + ].pos.withdrawalRequestPassTimestamp.toString(), + withdrawalRequestAmount: allPositionsData[ + index + ].pos.withdrawalRequestAmount.toString(), + transferPositionRequestPassTimestamp: allPositionsData[ + index + ].pos.transferPositionRequestPassTimestamp.toString(), + sponsor: { + __typename: "Sponsor", + id: item.args.sponsor, + }, + }; + setPositionsData((prevState: SponsorData[]) => { + return [...prevState, newPos]; + }); + }); + + setLoading(false); + }; + + // update positions data depends on emp + useEffect(() => { + querySponsorsData(); + }, [emp]); + + return { + positionsData, + loading, + }; +} + +const SponsorsData = createContainer(useSponsorsDataGetter); + +export default SponsorsData; diff --git a/pages/_app.tsx b/pages/_app.tsx index 81280bd..26eab9b 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -25,6 +25,7 @@ import { UniswapGetPair } from "../containers/Uniswap"; import ContractList from "../containers/ContractList"; import ContractState from "../containers/ContractState"; import LiquidationsData from "../containers/LiquidationsData"; +import SponsorsData from "../containers/SponsorsData"; import { ApolloProvider } from "@apollo/client"; import { client } from "../apollo/client"; @@ -46,27 +47,29 @@ const WithStateContainerProviders = ({ children }: IProps) => ( - - - - - - - - - - - {children} - - - - - - - - - - + + + + + + + + + + + + {children} + + + + + + + + + + + diff --git a/utils/events.ts b/utils/events.ts index ff2d4b1..a22b442 100644 --- a/utils/events.ts +++ b/utils/events.ts @@ -1,51 +1,89 @@ import { Contract } from "ethers"; import { contractToBlockCreated } from "./Contracts"; -export const getEventData = async (contract: Contract, eventName: string, filter: Array | [], startBlock?: number, endBlock?: number) => { +export const getEventData = async ( + contract: Contract, + eventName: string, + filter: Array | [], + startBlock?: number, + endBlock?: number +) => { + if (!startBlock || !endBlock) + return contract.queryFilter(contract.filters[eventName](...filter)); - if (!startBlock || !endBlock) return contract.queryFilter(contract.filters[eventName](...filter)); + let allEvents: any[] = []; + let from = startBlock; + let to = endBlock; + // Since query filter sometime fails and only works for up untill 5000 blocks, this works if block diff > 5000 b/w startBlock and endBlock. + for (let i = from; i < to; i += 5000) { + const _startBlock = i; + const _endBlock = Math.min(to, i + 4999); + const events = await contract.queryFilter( + contract.filters[eventName](...filter), + _startBlock, + _endBlock + ); + allEvents = [...allEvents, ...events]; + } + return allEvents; +}; - let allEvents: any[] = []; - let from = startBlock; - let to = endBlock; - // Since query filter sometime fails and only works for up untill 5000 blocks, this works if block diff > 5000 b/w startBlock and endBlock. - for (let i = from; i < to; i += 5000) { - const _startBlock = i; - const _endBlock = Math.min(to, i + 4999); - const events = await contract.queryFilter(contract.filters[eventName](...filter), _startBlock, _endBlock); - allEvents = [...allEvents, ...events] - } - return allEvents -} +export const getPositionCreatedEventData = async ( + contract: Contract, + filter: Array = [] +) => { + const start: number = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); -export const getLiquidationCreatedEventData = async (contract: Contract, filter: Array = []) => { - const start: number = contractToBlockCreated[contract.address.toLowerCase()]; - const end = await contract.provider.getBlockNumber(); + return getEventData(contract, "PositionCreated", filter, start, end); +}; - return getEventData(contract, "LiquidationCreated", filter, start, end) -} +export const getEndedSponsorPositionEventData = async ( + contract: Contract, + filter: Array = [] +) => { + const start: number = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); -export const getLiquidationDisputedEventData = async (contract: Contract, filter: Array = []) => { - const start = contractToBlockCreated[contract.address.toLowerCase()]; - const end = await contract.provider.getBlockNumber(); + return getEventData(contract, "EndedSponsorPosition", filter, start, end); +}; - return getEventData(contract, "LiquidationDisputed", filter, start, end) -} +export const getLiquidationCreatedEventData = async ( + contract: Contract, + filter: Array = [] +) => { + const start: number = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); -export const getDisputeSettledEventData = async (contract: Contract, filter: Array = []) => { - const start = contractToBlockCreated[contract.address.toLowerCase()]; - const end = await contract.provider.getBlockNumber(); + return getEventData(contract, "LiquidationCreated", filter, start, end); +}; - return getEventData(contract, "DisputeSettled", filter, start, end) -} +export const getLiquidationDisputedEventData = async ( + contract: Contract, + filter: Array = [] +) => { + const start = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); -export const getLiquidationWithdrawnEventData = async (contract: Contract, filter: Array = []) => { - const start = contractToBlockCreated[contract.address.toLowerCase()]; - const end = await contract.provider.getBlockNumber(); - - return getEventData(contract, "LiquidationWithdrawn", filter, start, end) -} + return getEventData(contract, "LiquidationDisputed", filter, start, end); +}; +export const getDisputeSettledEventData = async ( + contract: Contract, + filter: Array = [] +) => { + const start = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); + return getEventData(contract, "DisputeSettled", filter, start, end); +}; +export const getLiquidationWithdrawnEventData = async ( + contract: Contract, + filter: Array = [] +) => { + const start = contractToBlockCreated[contract.address.toLowerCase()]; + const end = await contract.provider.getBlockNumber(); + return getEventData(contract, "LiquidationWithdrawn", filter, start, end); +}; diff --git a/utils/getAbi.ts b/utils/getAbi.ts index 4686911..0248068 100644 --- a/utils/getAbi.ts +++ b/utils/getAbi.ts @@ -101,7 +101,7 @@ export const Contracts: ContractType[] = [ liquidationLiveness: (await instance.liquidationLiveness()) as BigNumber, withdrawalLiveness: (await instance.withdrawalLiveness()) as BigNumber, currentTime: (await instance.getCurrentTime()) as BigNumber, - // finderAddress: (await instance.finder()) as string, // address + finderAddress: (await instance.finder()) as string, // address // new // fundingRate: (await instance.fundingRate()) as BigNumber, // explicitly set null for missing fields