From cdf1d1c041ba1a5e9c40069e255d0dec0f2b90e2 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Wed, 21 Apr 2021 20:25:04 +0100 Subject: [PATCH 01/16] add subgraph entities for LQTY staking (enables historic LQTY staking queries) --- packages/subgraph/schema.graphql | 35 ++++++ packages/subgraph/src/entities/Global.ts | 22 +++- packages/subgraph/src/entities/LQTYStake.ts | 116 ++++++++++++++++++ packages/subgraph/src/entities/SystemState.ts | 4 +- packages/subgraph/src/mappings/LQTYStake.ts | 14 +++ packages/subgraph/subgraph.yaml | 28 +++++ 6 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 packages/subgraph/src/entities/LQTYStake.ts create mode 100644 packages/subgraph/src/mappings/LQTYStake.ts diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 966edf52e..43c3d0cca 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -16,6 +16,9 @@ type Global @entity { numberOfTrovesClosedByOwner: Int! totalNumberOfTroves: Int! + totalNumberOfLqtyStakes: Int! + totalLqtyTokensStaked: BigDecimal! + "Total redistributed per-stake collateral" rawTotalRedistributedCollateral: BigInt! @@ -56,6 +59,7 @@ type User @entity { trove: Trove stabilityDeposit: StabilityDeposit + stake: LqtyStake collSurplus: BigDecimal! collSurplusChanges: [CollSurplusChange!]! @derivedFrom(field: "user") @@ -246,3 +250,34 @@ type CollSurplusChange implements Change @entity { collSurplusChange: BigDecimal! collSurplusAfter: BigDecimal! } + +type LqtyStake @entity { + id: ID! + owner: User! + amount: BigDecimal! + changes: [LqtyStakeChange!]! @derivedFrom(field: "stake") +} + +enum LqtyStakeOperation { + stakeIncreased + stakeDecreased + gainsWithdrawn +} + +type LqtyStakeChange implements Change @entity { + id: ID! + sequenceNumber: Int! + transaction: Transaction! + systemStateBefore: SystemState! + systemStateAfter: SystemState! + + stake: LqtyStake! + operation: LqtyStakeOperation! + + amountBefore: BigDecimal! + amountChange: BigDecimal! + amountAfter: BigDecimal! + + issuanceGain: BigInt + redemptionGain: BigInt +} diff --git a/packages/subgraph/src/entities/Global.ts b/packages/subgraph/src/entities/Global.ts index 11659107a..70f4e9966 100644 --- a/packages/subgraph/src/entities/Global.ts +++ b/packages/subgraph/src/entities/Global.ts @@ -1,8 +1,8 @@ import { Value, BigInt, Address } from "@graphprotocol/graph-ts"; -import { Global } from "../../generated/schema"; +import { Global, LqtyStakeChange } from "../../generated/schema"; -import { BIGINT_ZERO } from "../utils/bignumbers"; +import { BIGINT_ZERO, DECIMAL_ZERO } from "../utils/bignumbers"; const onlyGlobalId = "only"; @@ -26,6 +26,8 @@ export function getGlobal(): Global { newGlobal.totalNumberOfTroves = 0; newGlobal.rawTotalRedistributedCollateral = BIGINT_ZERO; newGlobal.rawTotalRedistributedDebt = BIGINT_ZERO; + newGlobal.totalNumberOfLqtyStakes = 0; + newGlobal.totalLqtyTokensStaked = DECIMAL_ZERO; return newGlobal; } @@ -135,3 +137,19 @@ export function decreaseNumberOfTrovesClosedByOwner(): void { global.numberOfOpenTroves++; global.save(); } + +export function handleLqtyStakeChange(stakeChange: LqtyStakeChange): void { + let global = getGlobal(); + + if (stakeChange.operation === "newStake") { + global.totalNumberOfLqtyStakes++; + global.totalLqtyTokensStaked += stakeChange.amountChange; + } else if (stakeChange.operation === "stakeIncreased") { + global.totalLqtyTokensStaked += stakeChange.amountChange; + } else if (stakeChange.operation === "stakeDecreased") { + global.totalLqtyTokensStaked -= stakeChange.amountChange; + } else if (stakeChange.operation === "stakeRemoved") { + global.totalNumberOfLqtyStakes--; + global.totalLqtyTokensStaked -= stakeChange.amountChange; + } +} diff --git a/packages/subgraph/src/entities/LQTYStake.ts b/packages/subgraph/src/entities/LQTYStake.ts new file mode 100644 index 000000000..08a4724f5 --- /dev/null +++ b/packages/subgraph/src/entities/LQTYStake.ts @@ -0,0 +1,116 @@ +import { ethereum, Address, BigInt, BigDecimal } from "@graphprotocol/graph-ts"; + +import { LqtyStakeChange, LqtyStake } from "../../generated/schema"; + +import { decimalize, DECIMAL_ZERO, BIGINT_ZERO } from "../utils/bignumbers"; + +import { beginChange, initChange, finishChange } from "./Change"; +import { getUser } from "./User"; +import { handleLqtyStakeChange } from "./Global"; + +function beginLqtyStakeChange(event: ethereum.Event): LqtyStakeChange { + let sequenceNumber = beginChange(event); + let stakeChange = new LqtyStakeChange(sequenceNumber.toString()); + initChange(stakeChange, event, sequenceNumber); + return stakeChange; +} + +function finishLqtyStakeChange(stakeChange: LqtyStakeChange): void { + finishChange(stakeChange); + stakeChange.save(); +} + +function getUserStake(address: Address): LqtyStake | null { + let user = getUser(address); + + if (user.stake == null) { + return null; + } + + return LqtyStake.load(user.stake); +} + +function createStake(address: Address): LqtyStake { + let user = getUser(address); + let stake = new LqtyStake(address.toHexString()); + + stake.owner = user.id; + stake.amount = DECIMAL_ZERO; + + user.stake = stake.id; + user.save(); + + return stake; +} + +function getOperationType( + existingStake: LqtyStake | null, + stake: LqtyStake | null, + nextStakeAmount: BigDecimal +): string { + let isCreating = existingStake === null; + if (isCreating) { + return "stakeCreated"; + } + + let isIncreasing = nextStakeAmount > stake.amount; + if (isIncreasing) { + return "stakeIncreased"; + } + + let isRemoving = nextStakeAmount === DECIMAL_ZERO; + if (isRemoving) { + return "stakeRemoved"; + } + + return "stakeDecreased"; +} + +export function updateStake(event: ethereum.Event, address: Address, newStake: BigInt): void { + let existingStake = getUserStake(address); + let stake = existingStake; + + if (existingStake === null) { + stake = createStake(address); + } + + let nextStakeAmount = decimalize(newStake); + + let stakeChange = beginLqtyStakeChange(event); + stakeChange.stake = stake.id; + stakeChange.operation = getOperationType(existingStake, stake, nextStakeAmount); + stakeChange.amountBefore = stake.amount; + stakeChange.amountChange = stakeChange.amountAfter.minus(stakeChange.amountBefore); + stakeChange.amountAfter = nextStakeAmount; + stakeChange.issuanceGain = BIGINT_ZERO; + stakeChange.redemptionGain = BIGINT_ZERO; + + stake.amount = nextStakeAmount; + + handleLqtyStakeChange(stakeChange); + + finishLqtyStakeChange(stakeChange); + + stake.save(); +} + +export function withdrawStakeGains( + event: ethereum.Event, + address: Address, + LUSDGain: BigInt, + ETHGain: BigInt +): void { + if (LUSDGain == BIGINT_ZERO && ETHGain == BIGINT_ZERO) { + return; + } + + let stake = getUserStake(address) || createStake(address); + let stakeChange = beginLqtyStakeChange(event); + + stakeChange.issuanceGain = stakeChange.issuanceGain.minus(LUSDGain); + stakeChange.redemptionGain = stakeChange.redemptionGain.minus(ETHGain); + + finishLqtyStakeChange(stakeChange); + + stake.save(); +} diff --git a/packages/subgraph/src/entities/SystemState.ts b/packages/subgraph/src/entities/SystemState.ts index eece90ef6..78b8bea59 100644 --- a/packages/subgraph/src/entities/SystemState.ts +++ b/packages/subgraph/src/entities/SystemState.ts @@ -162,9 +162,7 @@ export function updateSystemStateByStabilityDepositChange( bumpSystemState(systemState); } -export function updateSystemStateByCollSurplusChange( - collSurplusChange: CollSurplusChange -): void { +export function updateSystemStateByCollSurplusChange(collSurplusChange: CollSurplusChange): void { let systemState = getCurrentSystemState(); systemState.collSurplusPoolBalance += collSurplusChange.collSurplusChange; diff --git a/packages/subgraph/src/mappings/LQTYStake.ts b/packages/subgraph/src/mappings/LQTYStake.ts new file mode 100644 index 000000000..c8c3dc5ba --- /dev/null +++ b/packages/subgraph/src/mappings/LQTYStake.ts @@ -0,0 +1,14 @@ +import { + StakeChanged, + StakingGainsWithdrawn +} from "../../generated/templates/LQTYStake/LQTYStaking"; + +import { updateStake, withdrawStakeGains } from "../entities/LqtyStake"; + +export function handleStakeChanged(event: StakeChanged): void { + updateStake(event, event.params.staker, event.params.newStake); +} + +export function handleStakeGainsWithdrawn(event: StakingGainsWithdrawn): void { + withdrawStakeGains(event, event.params.staker, event.params.LUSDGain, event.params.ETHGain); +} diff --git a/packages/subgraph/subgraph.yaml b/packages/subgraph/subgraph.yaml index 31e81859d..5197a3009 100644 --- a/packages/subgraph/subgraph.yaml +++ b/packages/subgraph/subgraph.yaml @@ -131,3 +131,31 @@ templates: eventHandlers: - event: CollBalanceUpdated(indexed address,uint256) handler: handleCollSurplusBalanceUpdated + - name: LqtyStake + kind: ethereum/contract + network: mainnet + source: + abi: LQTYStaking + mapping: + file: ./src/mappings/LQTYStake.ts + language: wasm/assemblyscript + kind: ethereum/events + apiVersion: 0.0.4 + entities: + - Global + - User + - Transaction + - PriceChange + - LqtyStake + - LqtyStakeChange + - SystemState + abis: + - name: LQTYStaking + file: ../lib-ethers/abi/LQTYStaking.json + - name: PriceFeed + file: ../lib-ethers/abi/PriceFeed.json + eventHandlers: + - event: StakeChanged(indexed address,uint256) + handler: handleStakeChanged + - event: StakingGainsWithdrawn(indexed address,uint256,uint256) + handler: handleStakeGainsWithdrawn From 9c1d25e5a1199d74764915a128b7608265fc7974 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Wed, 21 Apr 2021 20:51:10 +0100 Subject: [PATCH 02/16] remove gain changes from stake change event handler, the smart contract dispatches a separate withdraw event for gain changes --- packages/subgraph/src/entities/LQTYStake.ts | 2 -- packages/subgraph/subgraph.yaml | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/subgraph/src/entities/LQTYStake.ts b/packages/subgraph/src/entities/LQTYStake.ts index 08a4724f5..bab872879 100644 --- a/packages/subgraph/src/entities/LQTYStake.ts +++ b/packages/subgraph/src/entities/LQTYStake.ts @@ -82,8 +82,6 @@ export function updateStake(event: ethereum.Event, address: Address, newStake: B stakeChange.amountBefore = stake.amount; stakeChange.amountChange = stakeChange.amountAfter.minus(stakeChange.amountBefore); stakeChange.amountAfter = nextStakeAmount; - stakeChange.issuanceGain = BIGINT_ZERO; - stakeChange.redemptionGain = BIGINT_ZERO; stake.amount = nextStakeAmount; diff --git a/packages/subgraph/subgraph.yaml b/packages/subgraph/subgraph.yaml index 5197a3009..83e207c11 100644 --- a/packages/subgraph/subgraph.yaml +++ b/packages/subgraph/subgraph.yaml @@ -145,10 +145,8 @@ templates: - Global - User - Transaction - - PriceChange - LqtyStake - LqtyStakeChange - - SystemState abis: - name: LQTYStaking file: ../lib-ethers/abi/LQTYStaking.json From a8fdbf2420757be3e5e37e0840354d420f0a62c7 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Thu, 22 Apr 2021 15:12:51 +0100 Subject: [PATCH 03/16] add lqty-staking contract registration --- packages/subgraph/schema.graphql | 14 ++++---- packages/subgraph/src/entities/Global.ts | 26 +++++++------- packages/subgraph/src/entities/LQTYStake.ts | 34 +++++++++++-------- packages/subgraph/src/mappings/LQTYStake.ts | 2 +- .../subgraph/src/mappings/TroveManager.ts | 14 ++++++-- packages/subgraph/subgraph.yaml | 4 ++- 6 files changed, 56 insertions(+), 38 deletions(-) diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index 43c3d0cca..cd09a0933 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -16,8 +16,8 @@ type Global @entity { numberOfTrovesClosedByOwner: Int! totalNumberOfTroves: Int! - totalNumberOfLqtyStakes: Int! - totalLqtyTokensStaked: BigDecimal! + totalNumberOfLQTYStakes: Int! + totalLQTYTokensStaked: BigDecimal! "Total redistributed per-stake collateral" rawTotalRedistributedCollateral: BigInt! @@ -258,9 +258,11 @@ type LqtyStake @entity { changes: [LqtyStakeChange!]! @derivedFrom(field: "stake") } -enum LqtyStakeOperation { +enum LQTYStakeOperation { + stakeCreated stakeIncreased stakeDecreased + stakeRemoved gainsWithdrawn } @@ -272,12 +274,12 @@ type LqtyStakeChange implements Change @entity { systemStateAfter: SystemState! stake: LqtyStake! - operation: LqtyStakeOperation! + operation: LQTYStakeOperation! amountBefore: BigDecimal! amountChange: BigDecimal! amountAfter: BigDecimal! - issuanceGain: BigInt - redemptionGain: BigInt + issuanceGain: BigDecimal + redemptionGain: BigDecimal } diff --git a/packages/subgraph/src/entities/Global.ts b/packages/subgraph/src/entities/Global.ts index 70f4e9966..dce57d241 100644 --- a/packages/subgraph/src/entities/Global.ts +++ b/packages/subgraph/src/entities/Global.ts @@ -26,8 +26,8 @@ export function getGlobal(): Global { newGlobal.totalNumberOfTroves = 0; newGlobal.rawTotalRedistributedCollateral = BIGINT_ZERO; newGlobal.rawTotalRedistributedDebt = BIGINT_ZERO; - newGlobal.totalNumberOfLqtyStakes = 0; - newGlobal.totalLqtyTokensStaked = DECIMAL_ZERO; + newGlobal.totalNumberOfLQTYStakes = 0; + newGlobal.totalLQTYTokensStaked = DECIMAL_ZERO; return newGlobal; } @@ -138,18 +138,18 @@ export function decreaseNumberOfTrovesClosedByOwner(): void { global.save(); } -export function handleLqtyStakeChange(stakeChange: LqtyStakeChange): void { +export function handleLQTYStakeChange(stakeChange: LqtyStakeChange): void { let global = getGlobal(); - if (stakeChange.operation === "newStake") { - global.totalNumberOfLqtyStakes++; - global.totalLqtyTokensStaked += stakeChange.amountChange; - } else if (stakeChange.operation === "stakeIncreased") { - global.totalLqtyTokensStaked += stakeChange.amountChange; - } else if (stakeChange.operation === "stakeDecreased") { - global.totalLqtyTokensStaked -= stakeChange.amountChange; - } else if (stakeChange.operation === "stakeRemoved") { - global.totalNumberOfLqtyStakes--; - global.totalLqtyTokensStaked -= stakeChange.amountChange; + if (stakeChange.operation == "newStake") { + global.totalNumberOfLQTYStakes++; + global.totalLQTYTokensStaked += stakeChange.amountChange; + } else if (stakeChange.operation == "stakeIncreased") { + global.totalLQTYTokensStaked += stakeChange.amountChange; + } else if (stakeChange.operation == "stakeDecreased") { + global.totalLQTYTokensStaked -= stakeChange.amountChange; + } else if (stakeChange.operation == "stakeRemoved") { + global.totalNumberOfLQTYStakes--; + global.totalLQTYTokensStaked -= stakeChange.amountChange; } } diff --git a/packages/subgraph/src/entities/LQTYStake.ts b/packages/subgraph/src/entities/LQTYStake.ts index bab872879..1ce135b56 100644 --- a/packages/subgraph/src/entities/LQTYStake.ts +++ b/packages/subgraph/src/entities/LQTYStake.ts @@ -6,16 +6,16 @@ import { decimalize, DECIMAL_ZERO, BIGINT_ZERO } from "../utils/bignumbers"; import { beginChange, initChange, finishChange } from "./Change"; import { getUser } from "./User"; -import { handleLqtyStakeChange } from "./Global"; +import { handleLQTYStakeChange } from "./Global"; -function beginLqtyStakeChange(event: ethereum.Event): LqtyStakeChange { +function startLQTYStakeChange(event: ethereum.Event): LqtyStakeChange { let sequenceNumber = beginChange(event); let stakeChange = new LqtyStakeChange(sequenceNumber.toString()); initChange(stakeChange, event, sequenceNumber); return stakeChange; } -function finishLqtyStakeChange(stakeChange: LqtyStakeChange): void { +function finishLQTYStakeChange(stakeChange: LqtyStakeChange): void { finishChange(stakeChange); stakeChange.save(); } @@ -48,7 +48,7 @@ function getOperationType( stake: LqtyStake | null, nextStakeAmount: BigDecimal ): string { - let isCreating = existingStake === null; + let isCreating = existingStake == null; if (isCreating) { return "stakeCreated"; } @@ -58,7 +58,7 @@ function getOperationType( return "stakeIncreased"; } - let isRemoving = nextStakeAmount === DECIMAL_ZERO; + let isRemoving = nextStakeAmount == DECIMAL_ZERO; if (isRemoving) { return "stakeRemoved"; } @@ -70,24 +70,24 @@ export function updateStake(event: ethereum.Event, address: Address, newStake: B let existingStake = getUserStake(address); let stake = existingStake; - if (existingStake === null) { + if (existingStake == null) { stake = createStake(address); } let nextStakeAmount = decimalize(newStake); - let stakeChange = beginLqtyStakeChange(event); + let stakeChange = startLQTYStakeChange(event); stakeChange.stake = stake.id; stakeChange.operation = getOperationType(existingStake, stake, nextStakeAmount); stakeChange.amountBefore = stake.amount; - stakeChange.amountChange = stakeChange.amountAfter.minus(stakeChange.amountBefore); + stakeChange.amountChange = nextStakeAmount.minus(stake.amount); stakeChange.amountAfter = nextStakeAmount; stake.amount = nextStakeAmount; - handleLqtyStakeChange(stakeChange); + handleLQTYStakeChange(stakeChange); - finishLqtyStakeChange(stakeChange); + finishLQTYStakeChange(stakeChange); stake.save(); } @@ -103,12 +103,16 @@ export function withdrawStakeGains( } let stake = getUserStake(address) || createStake(address); - let stakeChange = beginLqtyStakeChange(event); - - stakeChange.issuanceGain = stakeChange.issuanceGain.minus(LUSDGain); - stakeChange.redemptionGain = stakeChange.redemptionGain.minus(ETHGain); + let stakeChange: LqtyStakeChange = startLQTYStakeChange(event); + stakeChange.stake = stake.id; + stakeChange.operation = "gainsWithdrawn"; + stakeChange.issuanceGain = stakeChange.issuanceGain.minus(decimalize(LUSDGain)); + stakeChange.redemptionGain = stakeChange.redemptionGain.minus(decimalize(ETHGain)); + stakeChange.amountBefore = stake.amount; + stakeChange.amountChange = DECIMAL_ZERO; + stakeChange.amountAfter = stake.amount; - finishLqtyStakeChange(stakeChange); + finishLQTYStakeChange(stakeChange); stake.save(); } diff --git a/packages/subgraph/src/mappings/LQTYStake.ts b/packages/subgraph/src/mappings/LQTYStake.ts index c8c3dc5ba..514a73973 100644 --- a/packages/subgraph/src/mappings/LQTYStake.ts +++ b/packages/subgraph/src/mappings/LQTYStake.ts @@ -1,7 +1,7 @@ import { StakeChanged, StakingGainsWithdrawn -} from "../../generated/templates/LQTYStake/LQTYStaking"; +} from "../../generated/templates/LqtyStake/LQTYStaking"; import { updateStake, withdrawStakeGains } from "../entities/LqtyStake"; diff --git a/packages/subgraph/src/mappings/TroveManager.ts b/packages/subgraph/src/mappings/TroveManager.ts index b18f9ae59..a954215d1 100644 --- a/packages/subgraph/src/mappings/TroveManager.ts +++ b/packages/subgraph/src/mappings/TroveManager.ts @@ -7,9 +7,15 @@ import { BorrowerOperationsAddressChanged, StabilityPoolAddressChanged, CollSurplusPoolAddressChanged, - PriceFeedAddressChanged + PriceFeedAddressChanged, + LQTYStakingAddressChanged } from "../../generated/TroveManager/TroveManager"; -import { BorrowerOperations, StabilityPool, CollSurplusPool } from "../../generated/templates"; +import { + BorrowerOperations, + StabilityPool, + CollSurplusPool, + LqtyStake +} from "../../generated/templates"; import { BIGINT_ZERO } from "../utils/bignumbers"; @@ -34,6 +40,10 @@ export function handleCollSurplusPoolAddressChanged(event: CollSurplusPoolAddres CollSurplusPool.create(event.params._collSurplusPoolAddress); } +export function handleLQTYStakingAddressChanged(event: LQTYStakingAddressChanged): void { + LqtyStake.create(event.params._lqtyStakingAddress); +} + export function handlePriceFeedAddressChanged(event: PriceFeedAddressChanged): void { updatePriceFeedAddress(event.params._newPriceFeedAddress); } diff --git a/packages/subgraph/subgraph.yaml b/packages/subgraph/subgraph.yaml index 83e207c11..5363a6491 100644 --- a/packages/subgraph/subgraph.yaml +++ b/packages/subgraph/subgraph.yaml @@ -41,6 +41,8 @@ dataSources: handler: handleCollSurplusPoolAddressChanged - event: PriceFeedAddressChanged(address) handler: handlePriceFeedAddressChanged + - event: LQTYStakingAddressChanged(address) + handler: handleLQTYStakingAddressChanged - event: TroveUpdated(indexed address,uint256,uint256,uint256,uint8) handler: handleTroveUpdated - event: TroveLiquidated(indexed address,uint256,uint256,uint8) @@ -137,7 +139,7 @@ templates: source: abi: LQTYStaking mapping: - file: ./src/mappings/LQTYStake.ts + file: ./src/mappings/LqtyStake.ts language: wasm/assemblyscript kind: ethereum/events apiVersion: 0.0.4 From cc1bfc751a4f3f3b14dfe8913f5a93b154c8411d Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Thu, 22 Apr 2021 16:53:39 +0100 Subject: [PATCH 04/16] correct file name casing --- packages/subgraph/src/entities/{LQTYStake.ts => LqtyStake.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/subgraph/src/entities/{LQTYStake.ts => LqtyStake.ts} (100%) diff --git a/packages/subgraph/src/entities/LQTYStake.ts b/packages/subgraph/src/entities/LqtyStake.ts similarity index 100% rename from packages/subgraph/src/entities/LQTYStake.ts rename to packages/subgraph/src/entities/LqtyStake.ts From cf5e2422a7d0703727ff7a5c3a6744ae837dbc22 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Thu, 22 Apr 2021 17:27:15 +0100 Subject: [PATCH 05/16] show lqty staking gains as a positive number, not negative --- packages/subgraph/src/entities/LqtyStake.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/subgraph/src/entities/LqtyStake.ts b/packages/subgraph/src/entities/LqtyStake.ts index 1ce135b56..5423c438a 100644 --- a/packages/subgraph/src/entities/LqtyStake.ts +++ b/packages/subgraph/src/entities/LqtyStake.ts @@ -106,8 +106,8 @@ export function withdrawStakeGains( let stakeChange: LqtyStakeChange = startLQTYStakeChange(event); stakeChange.stake = stake.id; stakeChange.operation = "gainsWithdrawn"; - stakeChange.issuanceGain = stakeChange.issuanceGain.minus(decimalize(LUSDGain)); - stakeChange.redemptionGain = stakeChange.redemptionGain.minus(decimalize(ETHGain)); + stakeChange.issuanceGain = decimalize(LUSDGain); + stakeChange.redemptionGain = decimalize(ETHGain); stakeChange.amountBefore = stake.amount; stakeChange.amountChange = DECIMAL_ZERO; stakeChange.amountAfter = stake.amount; From 2bf402f0a39c0d18236233493c756958218891e7 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Thu, 22 Apr 2021 18:01:26 +0100 Subject: [PATCH 06/16] fix global state tracking, simplify logic --- packages/subgraph/src/entities/Global.ts | 12 +++++++----- packages/subgraph/src/entities/LqtyStake.ts | 21 ++++++--------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/packages/subgraph/src/entities/Global.ts b/packages/subgraph/src/entities/Global.ts index dce57d241..efb0a2f7f 100644 --- a/packages/subgraph/src/entities/Global.ts +++ b/packages/subgraph/src/entities/Global.ts @@ -141,15 +141,17 @@ export function decreaseNumberOfTrovesClosedByOwner(): void { export function handleLQTYStakeChange(stakeChange: LqtyStakeChange): void { let global = getGlobal(); - if (stakeChange.operation == "newStake") { + if (stakeChange.operation == "stakeCreated") { global.totalNumberOfLQTYStakes++; - global.totalLQTYTokensStaked += stakeChange.amountChange; + global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.plus(stakeChange.amountChange); } else if (stakeChange.operation == "stakeIncreased") { - global.totalLQTYTokensStaked += stakeChange.amountChange; + global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.plus(stakeChange.amountChange); } else if (stakeChange.operation == "stakeDecreased") { - global.totalLQTYTokensStaked -= stakeChange.amountChange; + global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.minus(stakeChange.amountChange); } else if (stakeChange.operation == "stakeRemoved") { global.totalNumberOfLQTYStakes--; - global.totalLQTYTokensStaked -= stakeChange.amountChange; + global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.minus(stakeChange.amountChange); } + + global.save(); } diff --git a/packages/subgraph/src/entities/LqtyStake.ts b/packages/subgraph/src/entities/LqtyStake.ts index 5423c438a..ae6c192c1 100644 --- a/packages/subgraph/src/entities/LqtyStake.ts +++ b/packages/subgraph/src/entities/LqtyStake.ts @@ -24,7 +24,7 @@ function getUserStake(address: Address): LqtyStake | null { let user = getUser(address); if (user.stake == null) { - return null; + return createStake(address); } return LqtyStake.load(user.stake); @@ -43,12 +43,8 @@ function createStake(address: Address): LqtyStake { return stake; } -function getOperationType( - existingStake: LqtyStake | null, - stake: LqtyStake | null, - nextStakeAmount: BigDecimal -): string { - let isCreating = existingStake == null; +function getOperationType(stake: LqtyStake | null, nextStakeAmount: BigDecimal): string { + let isCreating = stake.amount == DECIMAL_ZERO && nextStakeAmount > DECIMAL_ZERO; if (isCreating) { return "stakeCreated"; } @@ -67,18 +63,13 @@ function getOperationType( } export function updateStake(event: ethereum.Event, address: Address, newStake: BigInt): void { - let existingStake = getUserStake(address); - let stake = existingStake; - - if (existingStake == null) { - stake = createStake(address); - } + let stake = getUserStake(address); let nextStakeAmount = decimalize(newStake); let stakeChange = startLQTYStakeChange(event); stakeChange.stake = stake.id; - stakeChange.operation = getOperationType(existingStake, stake, nextStakeAmount); + stakeChange.operation = getOperationType(stake, nextStakeAmount); stakeChange.amountBefore = stake.amount; stakeChange.amountChange = nextStakeAmount.minus(stake.amount); stakeChange.amountAfter = nextStakeAmount; @@ -102,7 +93,7 @@ export function withdrawStakeGains( return; } - let stake = getUserStake(address) || createStake(address); + let stake = getUserStake(address); let stakeChange: LqtyStakeChange = startLQTYStakeChange(event); stakeChange.stake = stake.id; stakeChange.operation = "gainsWithdrawn"; From 117246a1616217c7d2dd0632f5dc992c10794e50 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Thu, 22 Apr 2021 18:05:26 +0100 Subject: [PATCH 07/16] fix casing --- packages/subgraph/src/mappings/{LQTYStake.ts => LqtyStake.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/subgraph/src/mappings/{LQTYStake.ts => LqtyStake.ts} (100%) diff --git a/packages/subgraph/src/mappings/LQTYStake.ts b/packages/subgraph/src/mappings/LqtyStake.ts similarity index 100% rename from packages/subgraph/src/mappings/LQTYStake.ts rename to packages/subgraph/src/mappings/LqtyStake.ts From 41c68de47191506197f0d028195730790bd9078c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=9Fingen?= Date: Fri, 23 Apr 2021 12:16:33 +0200 Subject: [PATCH 08/16] subgraph: Rename subgraph --- packages/subgraph/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/subgraph/package.json b/packages/subgraph/package.json index 99d02a165..2d66ffbcd 100644 --- a/packages/subgraph/package.json +++ b/packages/subgraph/package.json @@ -3,12 +3,12 @@ "version": "0.1.0", "private": true, "scripts": { - "create": "graph create liquity/subgraph --node https://api.thegraph.com/deploy/", - "create-local": "graph create liquity/subgraph --node http://127.0.0.1:8020", + "create": "graph create liquity/liquity-protocol --node https://api.thegraph.com/deploy/", + "create-local": "graph create liquity/liquity-protocol --node http://127.0.0.1:8020", "prepare": "graph codegen", "build": "graph build", - "deploy": "graph deploy liquity/subgraph --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/", - "deploy-local": "graph deploy liquity/subgraph --ipfs http://localhost:5001 --node http://127.0.0.1:8020", + "deploy": "graph deploy liquity/liquity-protocol --ipfs https://api.thegraph.com/ipfs/ --node https://api.thegraph.com/deploy/", + "deploy-local": "graph deploy liquity/liquity-protocol --ipfs http://localhost:5001 --node http://127.0.0.1:8020", "graph": "graph" }, "devDependencies": { From b3b526b1c50735039440d6d3abc9159cb3e00e21 Mon Sep 17 00:00:00 2001 From: bojan-liquity <73836320+bojan-liquity@users.noreply.github.com> Date: Fri, 23 Apr 2021 14:52:37 +0200 Subject: [PATCH 09/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 42bc54bc5..1e8b1e773 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ The partially redeemed Trove is re-inserted into the sorted list of Troves, and ### Full redemption -A Trove is defined as “fully redeemed from” when the redemption has caused (debt-50) of its debt to absorb (debt-50) LUSD. Then, its 50 LUSD Liquidation Reserve is cancelled with its remaining 50 debt: the Liquidation Reserve is burned from the gas address, and the 50 debt is zero’d. +A Trove is defined as “fully redeemed from” when the redemption has caused (debt-50) of its debt to absorb (debt-200) LUSD. Then, its 200 LUSD Liquidation Reserve is cancelled with its remaining 200 debt: the Liquidation Reserve is burned from the gas address, and the 50 debt is zero’d. Before closing, we must handle the Trove’s **collateral surplus**: that is, the excess ETH collateral remaining after redemption, due to its initial over-collateralization. From 2af3eab210de4cc1d1f1b198dcc61dd54eb078b9 Mon Sep 17 00:00:00 2001 From: bojan-liquity <73836320+bojan-liquity@users.noreply.github.com> Date: Fri, 23 Apr 2021 14:53:19 +0200 Subject: [PATCH 10/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e8b1e773..d728a5ee4 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ The partially redeemed Trove is re-inserted into the sorted list of Troves, and ### Full redemption -A Trove is defined as “fully redeemed from” when the redemption has caused (debt-50) of its debt to absorb (debt-200) LUSD. Then, its 200 LUSD Liquidation Reserve is cancelled with its remaining 200 debt: the Liquidation Reserve is burned from the gas address, and the 50 debt is zero’d. +A Trove is defined as “fully redeemed from” when the redemption has caused (debt-200) of its debt to absorb (debt-200) LUSD. Then, its 200 LUSD Liquidation Reserve is cancelled with its remaining 200 debt: the Liquidation Reserve is burned from the gas address, and the 50 debt is zero’d. Before closing, we must handle the Trove’s **collateral surplus**: that is, the excess ETH collateral remaining after redemption, due to its initial over-collateralization. From 3d7c9e9e373957cd4eeb5b120e8b996dbfafff7e Mon Sep 17 00:00:00 2001 From: bojan-liquity <73836320+bojan-liquity@users.noreply.github.com> Date: Fri, 23 Apr 2021 14:55:37 +0200 Subject: [PATCH 11/16] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d728a5ee4..bd98c11de 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ The partially redeemed Trove is re-inserted into the sorted list of Troves, and ### Full redemption -A Trove is defined as “fully redeemed from” when the redemption has caused (debt-200) of its debt to absorb (debt-200) LUSD. Then, its 200 LUSD Liquidation Reserve is cancelled with its remaining 200 debt: the Liquidation Reserve is burned from the gas address, and the 50 debt is zero’d. +A Trove is defined as “fully redeemed from” when the redemption has caused (debt-200) of its debt to absorb (debt-200) LUSD. Then, its 200 LUSD Liquidation Reserve is cancelled with its remaining 200 debt: the Liquidation Reserve is burned from the gas address, and the 200 debt is zero’d. Before closing, we must handle the Trove’s **collateral surplus**: that is, the excess ETH collateral remaining after redemption, due to its initial over-collateralization. From 1d82d22b8cdb98adf4ea5795df8562319f5d33af Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Fri, 23 Apr 2021 14:51:28 +0100 Subject: [PATCH 12/16] address PR comments --- packages/subgraph/schema.graphql | 3 ++- packages/subgraph/src/entities/Global.ts | 21 ++++++++++++------- packages/subgraph/src/entities/LqtyStake.ts | 17 ++++++++++----- .../subgraph/src/mappings/TroveManager.ts | 4 ++-- packages/subgraph/subgraph.yaml | 2 +- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/packages/subgraph/schema.graphql b/packages/subgraph/schema.graphql index cd09a0933..af033de9a 100644 --- a/packages/subgraph/schema.graphql +++ b/packages/subgraph/schema.graphql @@ -17,6 +17,7 @@ type Global @entity { totalNumberOfTroves: Int! totalNumberOfLQTYStakes: Int! + numberOfActiveLQTYStakes: Int! totalLQTYTokensStaked: BigDecimal! "Total redistributed per-stake collateral" @@ -274,7 +275,7 @@ type LqtyStakeChange implements Change @entity { systemStateAfter: SystemState! stake: LqtyStake! - operation: LQTYStakeOperation! + stakeOperation: LQTYStakeOperation! amountBefore: BigDecimal! amountChange: BigDecimal! diff --git a/packages/subgraph/src/entities/Global.ts b/packages/subgraph/src/entities/Global.ts index efb0a2f7f..d649a5908 100644 --- a/packages/subgraph/src/entities/Global.ts +++ b/packages/subgraph/src/entities/Global.ts @@ -27,6 +27,7 @@ export function getGlobal(): Global { newGlobal.rawTotalRedistributedCollateral = BIGINT_ZERO; newGlobal.rawTotalRedistributedDebt = BIGINT_ZERO; newGlobal.totalNumberOfLQTYStakes = 0; + newGlobal.numberOfActiveLQTYStakes = 0; newGlobal.totalLQTYTokensStaked = DECIMAL_ZERO; return newGlobal; @@ -138,18 +139,24 @@ export function decreaseNumberOfTrovesClosedByOwner(): void { global.save(); } -export function handleLQTYStakeChange(stakeChange: LqtyStakeChange): void { +export function handleLQTYStakeChange( + stakeChange: LqtyStakeChange, + isUserFirstStake: boolean +): void { let global = getGlobal(); - if (stakeChange.operation == "stakeCreated") { - global.totalNumberOfLQTYStakes++; + if (stakeChange.stakeOperation == "stakeCreated") { + if (isUserFirstStake) { + global.totalNumberOfLQTYStakes++; + } + global.numberOfActiveLQTYStakes++; global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.plus(stakeChange.amountChange); - } else if (stakeChange.operation == "stakeIncreased") { + } else if (stakeChange.stakeOperation == "stakeIncreased") { global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.plus(stakeChange.amountChange); - } else if (stakeChange.operation == "stakeDecreased") { + } else if (stakeChange.stakeOperation == "stakeDecreased") { global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.minus(stakeChange.amountChange); - } else if (stakeChange.operation == "stakeRemoved") { - global.totalNumberOfLQTYStakes--; + } else if (stakeChange.stakeOperation == "stakeRemoved") { + global.numberOfActiveLQTYStakes--; global.totalLQTYTokensStaked = global.totalLQTYTokensStaked.minus(stakeChange.amountChange); } diff --git a/packages/subgraph/src/entities/LqtyStake.ts b/packages/subgraph/src/entities/LqtyStake.ts index ae6c192c1..fedeafaa6 100644 --- a/packages/subgraph/src/entities/LqtyStake.ts +++ b/packages/subgraph/src/entities/LqtyStake.ts @@ -11,6 +11,8 @@ import { handleLQTYStakeChange } from "./Global"; function startLQTYStakeChange(event: ethereum.Event): LqtyStakeChange { let sequenceNumber = beginChange(event); let stakeChange = new LqtyStakeChange(sequenceNumber.toString()); + stakeChange.issuanceGain = DECIMAL_ZERO; + stakeChange.redemptionGain = DECIMAL_ZERO; initChange(stakeChange, event, sequenceNumber); return stakeChange; } @@ -24,7 +26,7 @@ function getUserStake(address: Address): LqtyStake | null { let user = getUser(address); if (user.stake == null) { - return createStake(address); + return null; } return LqtyStake.load(user.stake); @@ -64,19 +66,24 @@ function getOperationType(stake: LqtyStake | null, nextStakeAmount: BigDecimal): export function updateStake(event: ethereum.Event, address: Address, newStake: BigInt): void { let stake = getUserStake(address); + let isUserFirstStake = stake == null; + + if (stake == null) { + stake = createStake(address); + } let nextStakeAmount = decimalize(newStake); let stakeChange = startLQTYStakeChange(event); stakeChange.stake = stake.id; - stakeChange.operation = getOperationType(stake, nextStakeAmount); + stakeChange.stakeOperation = getOperationType(stake, nextStakeAmount); stakeChange.amountBefore = stake.amount; stakeChange.amountChange = nextStakeAmount.minus(stake.amount); stakeChange.amountAfter = nextStakeAmount; stake.amount = nextStakeAmount; - handleLQTYStakeChange(stakeChange); + handleLQTYStakeChange(stakeChange, isUserFirstStake); finishLQTYStakeChange(stakeChange); @@ -93,10 +100,10 @@ export function withdrawStakeGains( return; } - let stake = getUserStake(address); + let stake = getUserStake(address) || createStake(address); let stakeChange: LqtyStakeChange = startLQTYStakeChange(event); stakeChange.stake = stake.id; - stakeChange.operation = "gainsWithdrawn"; + stakeChange.stakeOperation = "gainsWithdrawn"; stakeChange.issuanceGain = decimalize(LUSDGain); stakeChange.redemptionGain = decimalize(ETHGain); stakeChange.amountBefore = stake.amount; diff --git a/packages/subgraph/src/mappings/TroveManager.ts b/packages/subgraph/src/mappings/TroveManager.ts index a954215d1..ba6c8550f 100644 --- a/packages/subgraph/src/mappings/TroveManager.ts +++ b/packages/subgraph/src/mappings/TroveManager.ts @@ -14,7 +14,7 @@ import { BorrowerOperations, StabilityPool, CollSurplusPool, - LqtyStake + LQTYStaking } from "../../generated/templates"; import { BIGINT_ZERO } from "../utils/bignumbers"; @@ -41,7 +41,7 @@ export function handleCollSurplusPoolAddressChanged(event: CollSurplusPoolAddres } export function handleLQTYStakingAddressChanged(event: LQTYStakingAddressChanged): void { - LqtyStake.create(event.params._lqtyStakingAddress); + LQTYStaking.create(event.params._lqtyStakingAddress); } export function handlePriceFeedAddressChanged(event: PriceFeedAddressChanged): void { diff --git a/packages/subgraph/subgraph.yaml b/packages/subgraph/subgraph.yaml index 5363a6491..88a5c1c6d 100644 --- a/packages/subgraph/subgraph.yaml +++ b/packages/subgraph/subgraph.yaml @@ -133,7 +133,7 @@ templates: eventHandlers: - event: CollBalanceUpdated(indexed address,uint256) handler: handleCollSurplusBalanceUpdated - - name: LqtyStake + - name: LQTYStaking kind: ethereum/contract network: mainnet source: From b0b28d14b9ec7860b6a712d815b41b25ebb5e03f Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Fri, 23 Apr 2021 15:02:42 +0100 Subject: [PATCH 13/16] fix path --- packages/subgraph/src/mappings/LqtyStake.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/subgraph/src/mappings/LqtyStake.ts b/packages/subgraph/src/mappings/LqtyStake.ts index 514a73973..1b73443a4 100644 --- a/packages/subgraph/src/mappings/LqtyStake.ts +++ b/packages/subgraph/src/mappings/LqtyStake.ts @@ -1,7 +1,7 @@ import { StakeChanged, StakingGainsWithdrawn -} from "../../generated/templates/LqtyStake/LQTYStaking"; +} from "../../generated/templates/LQTYStaking/LQTYStaking"; import { updateStake, withdrawStakeGains } from "../entities/LqtyStake"; From 506b338776ffccb051c34b09f69436f32e2b9b15 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Mon, 26 Apr 2021 15:17:25 +0100 Subject: [PATCH 14/16] choose borrow amount when opening a trove --- .../src/components/Trove/Opening.tsx | 206 ++++++++++++++++++ .../src/components/Trove/Trove.tsx | 3 +- .../Trove/validation/validateTroveChange.tsx | 6 +- 3 files changed, 211 insertions(+), 4 deletions(-) create mode 100644 packages/dev-frontend/src/components/Trove/Opening.tsx diff --git a/packages/dev-frontend/src/components/Trove/Opening.tsx b/packages/dev-frontend/src/components/Trove/Opening.tsx new file mode 100644 index 000000000..f6e301d09 --- /dev/null +++ b/packages/dev-frontend/src/components/Trove/Opening.tsx @@ -0,0 +1,206 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Flex, Button, Box, Card, Heading } from "theme-ui"; + +import { + LiquityStoreState, + Decimal, + Trove, + LUSD_MINIMUM_DEBT, + LUSD_LIQUIDATION_RESERVE, + Percent +} from "@liquity/lib-base"; + +import { useLiquitySelector } from "@liquity/lib-react"; + +import { ActionDescription } from "../ActionDescription"; +import { useMyTransactionState } from "../Transaction"; + +import { TroveAction } from "./TroveAction"; +import { useTroveView } from "./context/TroveViewContext"; + +import { COIN } from "../../strings"; +import { Icon } from "../Icon"; +import { InfoIcon } from "../InfoIcon"; +import { LoadingOverlay } from "../LoadingOverlay"; +import { CollateralRatio } from "./CollateralRatio"; +import { EditableRow, StaticRow } from "./Editor"; +import { + selectForTroveChangeValidation, + validateTroveChange +} from "./validation/validateTroveChange"; + +const selector = (state: LiquityStoreState) => { + const { fees, price } = state; + return { + fees, + price, + validationContext: selectForTroveChangeValidation(state) + }; +}; +const EMPTY_TROVE = new Trove(Decimal.ZERO, Decimal.ZERO); +const TINY_EXTRA_LUSD_TO_ALLOW_MINIMUM = 0.0001; +const TRANSACTION_ID = "trove-creation"; + +export const Opening: React.FC = () => { + const { dispatchEvent } = useTroveView(); + const { fees, price, validationContext } = useLiquitySelector(selector); + const borrowingRate = fees.borrowingRate(); + const editingState = useState(); + + const minimumBorrowAmount = Decimal.from(LUSD_MINIMUM_DEBT.sub(LUSD_LIQUIDATION_RESERVE)) + .div(Decimal.from(1).add(borrowingRate)) + .add(TINY_EXTRA_LUSD_TO_ALLOW_MINIMUM); + const [collateral, setCollateral] = useState(Decimal.ZERO); + const [borrowAmount, setBorrowAmount] = useState(Decimal.ZERO); + + const maxBorrowingRate = borrowingRate.add(0.005); + + const fee = borrowAmount.mul(borrowingRate); + const feePct = new Percent(borrowingRate); + const totalDebt = borrowAmount.add(LUSD_LIQUIDATION_RESERVE).add(fee); + const isDirty = !collateral.isZero || !borrowAmount.isZero; + const trove = isDirty ? new Trove(collateral, totalDebt) : EMPTY_TROVE; + + const [troveChange, description] = validateTroveChange( + EMPTY_TROVE, + trove, + borrowingRate, + validationContext + ); + + const collateralRatio = + !collateral.isZero && !borrowAmount.isZero ? trove.collateralRatio(price) : undefined; + + const transactionState = useMyTransactionState(TRANSACTION_ID); + const isTransactionPending = + transactionState.type === "waitingForApproval" || + transactionState.type === "waitingForConfirmation"; + const handleCancelPressed = () => dispatchEvent("CANCEL_ADJUST_TROVE_PRESSED"); + + const reset = useCallback(() => { + setCollateral(Decimal.ZERO); + setBorrowAmount(Decimal.ZERO); + }, []); + + useEffect(() => { + if (!collateral.isZero && borrowAmount.isZero) { + setBorrowAmount(minimumBorrowAmount); + } + }, [collateral, borrowAmount, minimumBorrowAmount]); + + return ( + + + Trove + {isDirty && !isTransactionPending && ( + + )} + + + + setCollateral(Decimal.from(amount))} + /> + + setBorrowAmount(Decimal.from(amount))} + /> + + + An amount set aside to cover the liquidator’s gas costs if your Trove needs to be + liquidated. The amount increases your debt and is refunded if you close your Trove + by fully paying off its net debt. + + } + /> + } + /> + + + This amount is deducted from the borrowed amount as a one-time fee. There are no + recurring fees for borrowing, which is thus interest-free. + + } + /> + } + /> + + + TODO + + } + /> + } + /> + + + + {description ?? ( + + Start by entering the amount of ETH you'd like to deposit as collateral. + + )} + + + + + {troveChange ? ( + + Confirm + + ) : ( + + )} + + + {isTransactionPending && } + + ); +}; diff --git a/packages/dev-frontend/src/components/Trove/Trove.tsx b/packages/dev-frontend/src/components/Trove/Trove.tsx index 874d90690..8ddec6f0c 100644 --- a/packages/dev-frontend/src/components/Trove/Trove.tsx +++ b/packages/dev-frontend/src/components/Trove/Trove.tsx @@ -2,6 +2,7 @@ import React from "react"; import { TroveManager } from "./TroveManager"; import { ReadOnlyTrove } from "./ReadOnlyTrove"; import { NoTrove } from "./NoTrove"; +import { Opening } from "./Opening"; import { RedeemedTrove } from "./RedeemedTrove"; import { useTroveView } from "./context/TroveViewContext"; import { LiquidatedTrove } from "./LiquidatedTrove"; @@ -22,7 +23,7 @@ export const Trove: React.FC = props => { return ; } case "OPENING": { - return ; + return ; } case "LIQUIDATED": { return ; diff --git a/packages/dev-frontend/src/components/Trove/validation/validateTroveChange.tsx b/packages/dev-frontend/src/components/Trove/validation/validateTroveChange.tsx index 32bbc5837..ec012d7d3 100644 --- a/packages/dev-frontend/src/components/Trove/validation/validateTroveChange.tsx +++ b/packages/dev-frontend/src/components/Trove/validation/validateTroveChange.tsx @@ -137,7 +137,7 @@ export const validateTroveChange = ( return [ undefined, - Debt must be at least{" "} + Total debt must be at least{" "} {LUSD_MINIMUM_DEBT.toString()} {COIN} @@ -173,7 +173,7 @@ const validateTroveCreation = ( if (resultingTrove.debt.lt(LUSD_MINIMUM_DEBT)) { return ( - Debt must be at least{" "} + Total debt must be at least{" "} {LUSD_MINIMUM_DEBT.toString()} {COIN} @@ -284,7 +284,7 @@ const validateTroveAdjustment = ( if (resultingTrove.debt.lt(LUSD_MINIMUM_DEBT)) { return ( - Debt must be at least{" "} + Total debt must be at least{" "} {LUSD_MINIMUM_DEBT.toString()} {COIN} From 69619a60c57ff1b9580b9fc55ce6b9d0c4fc534a Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Mon, 26 Apr 2021 16:14:53 +0100 Subject: [PATCH 15/16] update tooltip, include gas reservation when pressing max on collateral field --- .../src/components/Trove/Opening.tsx | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/dev-frontend/src/components/Trove/Opening.tsx b/packages/dev-frontend/src/components/Trove/Opening.tsx index f6e301d09..3ed038471 100644 --- a/packages/dev-frontend/src/components/Trove/Opening.tsx +++ b/packages/dev-frontend/src/components/Trove/Opening.tsx @@ -1,6 +1,5 @@ import React, { useCallback, useEffect, useState } from "react"; import { Flex, Button, Box, Card, Heading } from "theme-ui"; - import { LiquityStoreState, Decimal, @@ -9,15 +8,11 @@ import { LUSD_LIQUIDATION_RESERVE, Percent } from "@liquity/lib-base"; - import { useLiquitySelector } from "@liquity/lib-react"; - import { ActionDescription } from "../ActionDescription"; import { useMyTransactionState } from "../Transaction"; - import { TroveAction } from "./TroveAction"; import { useTroveView } from "./context/TroveViewContext"; - import { COIN } from "../../strings"; import { Icon } from "../Icon"; import { InfoIcon } from "../InfoIcon"; @@ -30,20 +25,23 @@ import { } from "./validation/validateTroveChange"; const selector = (state: LiquityStoreState) => { - const { fees, price } = state; + const { fees, price, accountBalance } = state; return { fees, price, + accountBalance, validationContext: selectForTroveChangeValidation(state) }; }; + const EMPTY_TROVE = new Trove(Decimal.ZERO, Decimal.ZERO); const TINY_EXTRA_LUSD_TO_ALLOW_MINIMUM = 0.0001; const TRANSACTION_ID = "trove-creation"; +const GAS_ROOM_ETH = Decimal.from(0.1); export const Opening: React.FC = () => { const { dispatchEvent } = useTroveView(); - const { fees, price, validationContext } = useLiquitySelector(selector); + const { fees, price, accountBalance, validationContext } = useLiquitySelector(selector); const borrowingRate = fees.borrowingRate(); const editingState = useState(); @@ -60,6 +58,11 @@ export const Opening: React.FC = () => { const totalDebt = borrowAmount.add(LUSD_LIQUIDATION_RESERVE).add(fee); const isDirty = !collateral.isZero || !borrowAmount.isZero; const trove = isDirty ? new Trove(collateral, totalDebt) : EMPTY_TROVE; + const maxEth = accountBalance.gt(GAS_ROOM_ETH) ? accountBalance.sub(GAS_ROOM_ETH) : Decimal.ZERO; + const maxCollateral = collateral.add(maxEth); + const collateralMaxedOut = collateral.eq(maxCollateral); + const collateralRatio = + !collateral.isZero && !borrowAmount.isZero ? trove.collateralRatio(price) : undefined; const [troveChange, description] = validateTroveChange( EMPTY_TROVE, @@ -68,14 +71,14 @@ export const Opening: React.FC = () => { validationContext ); - const collateralRatio = - !collateral.isZero && !borrowAmount.isZero ? trove.collateralRatio(price) : undefined; - const transactionState = useMyTransactionState(TRANSACTION_ID); const isTransactionPending = transactionState.type === "waitingForApproval" || transactionState.type === "waitingForConfirmation"; - const handleCancelPressed = () => dispatchEvent("CANCEL_ADJUST_TROVE_PRESSED"); + + const handleCancelPressed = useCallback(() => { + dispatchEvent("CANCEL_ADJUST_TROVE_PRESSED"); + }, [dispatchEvent]); const reset = useCallback(() => { setCollateral(Decimal.ZERO); @@ -104,8 +107,8 @@ export const Opening: React.FC = () => { label="Collateral" inputId="trove-collateral" amount={collateral.prettify(4)} - // maxAmount={maxCollateral.toString()} - // maxedOut={collateralMaxedOut} + maxAmount={maxCollateral.toString()} + maxedOut={collateralMaxedOut} editingState={editingState} unit="ETH" editedAmount={collateral.toString(4)} @@ -167,7 +170,14 @@ export const Opening: React.FC = () => { - TODO + The total amount of LUSD your Trove will hold.{" "} + {isDirty && ( + <> + You will need to repay {totalDebt.sub(LUSD_LIQUIDATION_RESERVE).prettify(2)}{" "} + LUSD to reclaim your collateral ({LUSD_LIQUIDATION_RESERVE.toString()} LUSD + Liquidation Reserve excluded). + + )} } /> From e891dde9a6b2047d66acebb7b3a8015999198eb5 Mon Sep 17 00:00:00 2001 From: Edward Mulraney Date: Mon, 26 Apr 2021 17:52:50 +0100 Subject: [PATCH 16/16] choose net debt instead of total debt on adjust trove view --- packages/dev-frontend/src/App.test.tsx | 4 +- .../src/components/Trove/Adjusting.tsx | 227 ++++++++++++++++++ .../src/components/Trove/Trove.tsx | 3 +- .../src/components/Trove/TroveEditor.tsx | 37 +-- 4 files changed, 237 insertions(+), 34 deletions(-) create mode 100644 packages/dev-frontend/src/components/Trove/Adjusting.tsx diff --git a/packages/dev-frontend/src/App.test.tsx b/packages/dev-frontend/src/App.test.tsx index 4be95f507..3b06dce92 100644 --- a/packages/dev-frontend/src/App.test.tsx +++ b/packages/dev-frontend/src/App.test.tsx @@ -21,8 +21,8 @@ test("there's no smoke", async () => { fireEvent.click(getByText(/open trove/i)); fireEvent.click(getByLabelText(/collateral/i)); fireEvent.change(getByLabelText(/^collateral$/i), { target: { value: `${trove.collateral}` } }); - fireEvent.click(getByLabelText(/^debt$/i)); - fireEvent.change(getByLabelText(/^debt$/i), { target: { value: `${trove.debt}` } }); + fireEvent.click(getByLabelText(/^borrow$/i)); + fireEvent.change(getByLabelText(/^borrow$/i), { target: { value: `${trove.debt}` } }); const confirmButton = getAllByText(/confirm/i)[0]; fireEvent.click(confirmButton); diff --git a/packages/dev-frontend/src/components/Trove/Adjusting.tsx b/packages/dev-frontend/src/components/Trove/Adjusting.tsx new file mode 100644 index 000000000..e944b38a6 --- /dev/null +++ b/packages/dev-frontend/src/components/Trove/Adjusting.tsx @@ -0,0 +1,227 @@ +import React, { useCallback, useEffect, useState } from "react"; +import { Flex, Button, Box, Card, Heading } from "theme-ui"; +import { + LiquityStoreState, + Decimal, + Trove, + LUSD_LIQUIDATION_RESERVE, + Percent, + Difference +} from "@liquity/lib-base"; +import { useLiquitySelector } from "@liquity/lib-react"; +import { ActionDescription } from "../ActionDescription"; +import { useMyTransactionState } from "../Transaction"; +import { TroveAction } from "./TroveAction"; +import { useTroveView } from "./context/TroveViewContext"; +import { COIN } from "../../strings"; +import { Icon } from "../Icon"; +import { InfoIcon } from "../InfoIcon"; +import { LoadingOverlay } from "../LoadingOverlay"; +import { CollateralRatio } from "./CollateralRatio"; +import { EditableRow, StaticRow } from "./Editor"; +import { + selectForTroveChangeValidation, + validateTroveChange +} from "./validation/validateTroveChange"; + +const selector = (state: LiquityStoreState) => { + const { trove, fees, price, accountBalance } = state; + return { + trove, + fees, + price, + accountBalance, + validationContext: selectForTroveChangeValidation(state) + }; +}; + +const TRANSACTION_ID = "trove-adjustment"; +const GAS_ROOM_ETH = Decimal.from(0.1); + +const feeFrom = (original: Trove, edited: Trove, borrowingRate: Decimal): Decimal => { + const change = original.whatChanged(edited, borrowingRate); + + if (change && change.type !== "invalidCreation" && change.params.borrowLUSD) { + return change.params.borrowLUSD.mul(borrowingRate); + } else { + return Decimal.ZERO; + } +}; + +export const Adjusting: React.FC = () => { + const { dispatchEvent } = useTroveView(); + const { trove, fees, price, accountBalance, validationContext } = useLiquitySelector(selector); + const borrowingRate = fees.borrowingRate(); + const editingState = useState(); + const originalNetDebt = trove.debt.sub(LUSD_LIQUIDATION_RESERVE); + + const [collateral, setCollateral] = useState(trove.collateral); + const [netDebt, setNetDebt] = useState(originalNetDebt); + const isDirty = !collateral.eq(trove.collateral) || !netDebt.eq(originalNetDebt); + const isDebtIncrease = netDebt.gt(originalNetDebt); + const debtIncreaseAmount = isDebtIncrease ? netDebt.sub(originalNetDebt) : Decimal.ZERO; + + const fee = isDebtIncrease + ? feeFrom(trove, new Trove(trove.collateral, trove.debt.add(debtIncreaseAmount)), borrowingRate) + : Decimal.ZERO; + const totalDebt = netDebt.add(LUSD_LIQUIDATION_RESERVE).add(fee); + const maxBorrowingRate = borrowingRate.add(0.005); + const updatedTrove = isDirty ? new Trove(collateral, totalDebt) : trove; + const feePct = new Percent(borrowingRate); + const maxEth = accountBalance.gt(GAS_ROOM_ETH) ? accountBalance.sub(GAS_ROOM_ETH) : Decimal.ZERO; + const maxCollateral = collateral.add(maxEth); + const collateralMaxedOut = collateral.eq(maxCollateral); + const collateralRatio = + !collateral.isZero && !netDebt.isZero ? updatedTrove.collateralRatio(price) : undefined; + const collateralRatioChange = Difference.between(collateralRatio, trove.collateralRatio(price)); + + const [troveChange, description] = validateTroveChange( + trove, + updatedTrove, + borrowingRate, + validationContext + ); + + const transactionState = useMyTransactionState(TRANSACTION_ID); + const isTransactionPending = + transactionState.type === "waitingForApproval" || + transactionState.type === "waitingForConfirmation"; + + const handleCancelPressed = useCallback(() => { + dispatchEvent("CANCEL_ADJUST_TROVE_PRESSED"); + }, [dispatchEvent]); + + const reset = useCallback(() => { + setCollateral(trove.collateral); + setNetDebt(originalNetDebt); + }, [trove.collateral, originalNetDebt]); + + useEffect(() => { + if (transactionState.type === "confirmedOneShot") { + dispatchEvent("TROVE_ADJUSTED"); + } + }, [transactionState.type, dispatchEvent]); + + return ( + + + Trove + {isDirty && !isTransactionPending && ( + + )} + + + + setCollateral(Decimal.from(amount))} + /> + + setNetDebt(Decimal.from(amount))} + /> + + + An amount set aside to cover the liquidator’s gas costs if your Trove needs to be + liquidated. The amount increases your debt and is refunded if you close your Trove + by fully paying off its net debt. + + } + /> + } + /> + + + This amount is deducted from the borrowed amount as a one-time fee. There are no + recurring fees for borrowing, which is thus interest-free. + + } + /> + } + /> + + + The total amount of LUSD your Trove will hold.{" "} + {isDirty && ( + <> + You will need to repay {totalDebt.sub(LUSD_LIQUIDATION_RESERVE).prettify(2)}{" "} + LUSD to reclaim your collateral ({LUSD_LIQUIDATION_RESERVE.toString()} LUSD + Liquidation Reserve excluded). + + )} + + } + /> + } + /> + + + + {description ?? ( + + Adjust your Trove by modifying its collateral, debt, or both. + + )} + + + + + {troveChange ? ( + + Confirm + + ) : ( + + )} + + + {isTransactionPending && } + + ); +}; diff --git a/packages/dev-frontend/src/components/Trove/Trove.tsx b/packages/dev-frontend/src/components/Trove/Trove.tsx index 8ddec6f0c..70350b1e3 100644 --- a/packages/dev-frontend/src/components/Trove/Trove.tsx +++ b/packages/dev-frontend/src/components/Trove/Trove.tsx @@ -3,6 +3,7 @@ import { TroveManager } from "./TroveManager"; import { ReadOnlyTrove } from "./ReadOnlyTrove"; import { NoTrove } from "./NoTrove"; import { Opening } from "./Opening"; +import { Adjusting } from "./Adjusting"; import { RedeemedTrove } from "./RedeemedTrove"; import { useTroveView } from "./context/TroveViewContext"; import { LiquidatedTrove } from "./LiquidatedTrove"; @@ -17,7 +18,7 @@ export const Trove: React.FC = props => { return ; } case "ADJUSTING": { - return ; + return ; } case "CLOSING": { return ; diff --git a/packages/dev-frontend/src/components/Trove/TroveEditor.tsx b/packages/dev-frontend/src/components/Trove/TroveEditor.tsx index 34c2af2e9..6adf8e79a 100644 --- a/packages/dev-frontend/src/components/Trove/TroveEditor.tsx +++ b/packages/dev-frontend/src/components/Trove/TroveEditor.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React from "react"; import { Heading, Box, Card, Button } from "theme-ui"; import { @@ -15,13 +15,11 @@ import { useLiquitySelector } from "@liquity/lib-react"; import { COIN } from "../../strings"; import { Icon } from "../Icon"; -import { EditableRow, StaticRow } from "./Editor"; +import { StaticRow } from "./Editor"; import { LoadingOverlay } from "../LoadingOverlay"; import { CollateralRatio } from "./CollateralRatio"; import { InfoIcon } from "../InfoIcon"; -const gasRoomETH = Decimal.from(0.1); - type TroveEditorProps = { original: Trove; edited: Trove; @@ -33,7 +31,7 @@ type TroveEditorProps = { ) => void; }; -const select = ({ price, accountBalance }: LiquityStoreState) => ({ price, accountBalance }); +const select = ({ price }: LiquityStoreState) => ({ price }); export const TroveEditor: React.FC = ({ children, @@ -44,9 +42,7 @@ export const TroveEditor: React.FC = ({ changePending, dispatch }) => { - const { price, accountBalance } = useLiquitySelector(select); - - const editingState = useState(); + const { price } = useLiquitySelector(select); const feePct = new Percent(borrowingRate); @@ -54,10 +50,6 @@ export const TroveEditor: React.FC = ({ const collateralRatio = !edited.isEmpty ? edited.collateralRatio(price) : undefined; const collateralRatioChange = Difference.between(collateralRatio, originalCollateralRatio); - const maxEth = accountBalance.gt(gasRoomETH) ? accountBalance.sub(gasRoomETH) : Decimal.ZERO; - const maxCollateral = original.collateral.add(maxEth); - const collateralMaxedOut = edited.collateral.eq(maxCollateral); - const dirty = !edited.equals(original); return ( @@ -76,31 +68,14 @@ export const TroveEditor: React.FC = ({ - - dispatch({ type: "setCollateral", newValue: editedCollateral }) - } /> - - dispatch({ type: "setDebt", newValue: editedDebt }) - } - /> + {original.isEmpty && (