Skip to content

Commit

Permalink
mattupham/collect and reinvest (osmosis-labs#2136)
Browse files Browse the repository at this point in the history
* base func

* Add tsdoc

* Add coin and types to call

* Remove space

* Test collect and reinvest

* Convert to CoinPretty

* Update buttons

* Update add button disabled states

* Remove LD redirect

* get pools in price store (osmosis-labs#2166)

* Enable redirect

* Rename

---------

Co-authored-by: Jon Ator <[email protected]>
  • Loading branch information
mattupham and jonator authored Sep 19, 2023
1 parent 663f522 commit fe36573
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 51 deletions.
22 changes: 22 additions & 0 deletions packages/stores/src/account/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,28 @@ export class AccountStore<Injects extends Record<string, any>[] = []> {
return new Error(errorMessage);
}

/**
* Signs a transaction message and broadcasts it to the specified blockchain.
*
* @param chainNameOrId - Chain name or ID where the transaction will be broadcasted.
* @param type - Type of the transaction - this string is used to identify the transaction going through the pipeline.
* @param msgs - Array of messages to be included in the transaction or a function that returns such array.
* @param memo - Optional memo for the transaction. Default is an empty string.
* @param fee - Optional transaction fee details, if not provided the fee will be estimated.
* @param _signOptions - Optional Keplr sign options for customizing the sign process.
* @param onTxEvents - Optional callback or set of callbacks to be called based on transaction lifecycle events:
* - `onBroadcastFailed`: Invoked when the broadcast fails.
* - `onBroadcasted`: Invoked when the transaction is successfully broadcasted.
* - `onFulfill`: Invoked when the transaction is successfully fulfilled.
*
* @throws {Error} Throws an error if:
* - Wallet for the given chain is not provided or not connected.
* - There are no messages to send.
* - Wallet address is missing.
* - Broadcasting the transaction fails.
*
* @returns {Promise<void>} Resolves when the transaction is broadcasted and all events are processed, otherwise it rejects.
*/
async signAndBroadcast(
chainNameOrId: string,
type: string | "unknown",
Expand Down
55 changes: 54 additions & 1 deletion packages/stores/src/account/osmosis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2437,7 +2437,6 @@ export class OsmosisAccountImpl {
if (!tx.code) {
// Refresh the balances
const queries = this.queriesStore.get(this.chainId);

queries.queryBalances
.getQueryBech32Address(this.address)
.balances.forEach((balance) => balance.waitFreshResponse());
Expand All @@ -2460,6 +2459,60 @@ export class OsmosisAccountImpl {
);
}

/**
* Method to withdraw delegation rewards and delegate to validator set - staking collect and reinvest
* @param coin The coin object with denom and amount to delegate.
* @param memo Transaction memo.
* @param onFulfill Callback to handle tx fulfillment given raw response.
*/
async sendWithdrawDelegationRewardsAndSendDelegateToValidatorSetMsgs(
coin: { amount: string; denom: Currency },
memo: string = "",
onFulfill?: (tx: DeliverTxResponse) => void
) {
const withdrawDelegationRewardsMsg =
this.msgOpts.withdrawDelegationRewards.messageComposer({
delegator: this.address,
});

const delegateToValidatorSetMsg =
this.msgOpts.delegateToValidatorSet.messageComposer({
delegator: this.address,
coin: {
denom: coin.denom.coinMinimalDenom,
amount: coin.amount,
},
});

await this.base.signAndBroadcast(
this.chainId,
"withdrawDelegationRewardsAndSendDelegateToValidatorSet",
[withdrawDelegationRewardsMsg, delegateToValidatorSetMsg],
memo,
undefined,
undefined,
(tx) => {
if (!tx.code) {
// Refresh the balances
const queries = this.queriesStore.get(this.chainId);

queries.queryBalances
.getQueryBech32Address(this.address)
.balances.forEach((balance) => balance.waitFreshResponse());

queries.cosmos.queryDelegations
.getQueryBech32Address(this.address)
.waitFreshResponse();

queries.cosmos.queryRewards
.getQueryBech32Address(this.address)
.waitFreshResponse();
}
onFulfill?.(tx);
}
);
}

protected get queries() {
// eslint-disable-next-line
return this.queriesStore.get(this.chainId).osmosis!;
Expand Down
5 changes: 4 additions & 1 deletion packages/stores/src/price/pool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ export class PoolFallbackPriceStore
vsCurrency = this.defaultVsCurrency;
}

if (!this.queryPools.response) return;
if (!this.queryPools.response) {
this.queryPools.getAllPools();
return;
}

try {
const route = this._intermediateRoutesMap.get(coinId);
Expand Down
40 changes: 25 additions & 15 deletions packages/web/components/cards/rewards-card.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,49 @@
import classNames from "classnames";
import React from "react";

import { Icon } from "~/components/assets";
import { Button } from "~/components/buttons";
import { Tooltip } from "~/components/tooltip";

export const RewardsCard: React.FC<{
title: string;
tooltipContent: string;
disabledTooltipContent?: string;
onClick: () => void;
image?: JSX.Element;
containerClasses?: string;
disabled: boolean;
}> = ({
title,
tooltipContent,
disabledTooltipContent,
onClick,
image = null,
containerClasses = "",
disabled,
}) => {
return (
<div
className={classNames(
"flex w-full flex-grow cursor-pointer flex-col rounded-xl border-2 border-osmoverse-600",
containerClasses
)}
<Button
disabled={disabled}
mode="unstyled"
className="relative flex min-h-[50px] w-full flex-grow cursor-pointer flex-col !items-end justify-start overflow-hidden rounded-xl border-2 border-osmoverse-600 !p-0 disabled:cursor-not-allowed disabled:opacity-75"
onClick={onClick}
>
{image}
<div className="relative z-10 flex items-center justify-end p-4">
<div className="z-10 flex items-center gap-2 p-4">
<span className="text-osmoverse-white text-sm">{title}</span>
<div className="pl-2 text-osmoverse-600 sm:hidden">
<Tooltip content={tooltipContent}>
<Icon id="info" height="14px" width="14px" fill="#958FC0" />
</Tooltip>
</div>
{disabled && (
<div className="text-osmoverse-600 sm:hidden">
<Tooltip content={disabledTooltipContent}>
<Icon id="info" height="14px" width="14px" fill="#EF3456" />
</Tooltip>
</div>
)}
{!disabled && (
<div className="text-osmoverse-600 sm:hidden">
<Tooltip content={tooltipContent}>
<Icon id="info" height="14px" width="14px" fill="#958FC0" />
</Tooltip>
</div>
)}
</div>
</div>
</Button>
);
};
102 changes: 68 additions & 34 deletions packages/web/components/cards/stake-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Staking } from "@keplr-wallet/stores";
import { CoinPretty, Dec } from "@keplr-wallet/unit";
import { Currency } from "@keplr-wallet/types";
import { CoinPretty, Dec, PricePretty } from "@keplr-wallet/unit";
import { DeliverTxResponse } from "@osmosis-labs/stores";
import { observer } from "mobx-react-lite";
import React, { useCallback } from "react";
Expand All @@ -10,7 +11,7 @@ import { GenericMainCard } from "~/components/cards/generic-main-card";
import { RewardsCard } from "~/components/cards/rewards-card";
import { ValidatorSquadCard } from "~/components/cards/validator-squad-card";
import { EventName } from "~/config";
import { useAmplitudeAnalytics } from "~/hooks";
import { useAmplitudeAnalytics, useFakeFeeConfig } from "~/hooks";
import { useStore } from "~/stores";

export const StakeDashboard: React.FC<{
Expand All @@ -29,22 +30,23 @@ export const StakeDashboard: React.FC<{
const account = accountStore.getWallet(osmosisChainId);
const address = account?.address ?? "";
const osmo = chainStore.osmosis.stakeCurrency;
const fiat = priceStore.getFiatCurrency(priceStore.defaultVsCurrency)!;

const { rewards } =
cosmosQueries.queryRewards.getQueryBech32Address(address);

const summedStakeRewards = rewards?.reduce((acc, reward) => {
return reward.toDec().add(acc);
}, new Dec(0));

const coinPrettyStakeRewards = summedStakeRewards
? new CoinPretty(osmo, summedStakeRewards)
: new CoinPretty(osmo, 0);
return reward.add(acc);
}, new CoinPretty(osmo, 0));

const fiatRewards =
priceStore.calculatePrice(coinPrettyStakeRewards) || "0";
priceStore.calculatePrice(summedStakeRewards) || new PricePretty(fiat, 0);

const fiatBalance = balance
? priceStore.calculatePrice(balance)
: undefined;

const fiatBalance = balance ? priceStore.calculatePrice(balance) : 0;
const osmoRewardsAmount = summedStakeRewards.toCoin().amount;

const icon = (
<div className="flex items-center justify-center text-bullish-500">
Expand All @@ -70,35 +72,66 @@ export const StakeDashboard: React.FC<{
}
}, [account, logEvent]);

const gasForecastedCollectRewards = 2901105; // estimate based on gas simulation to run collect succesfully
const gasForecastedCollectAndReinvestRewards = 6329136; // estimate based on gas simulation to run collect and reinvest succesfully

const { fee: collectRewardsFee } = useFakeFeeConfig(
chainStore,
chainStore.osmosis.chainId,
gasForecastedCollectRewards
);

const { fee: collectAndReinvestRewardsFee } = useFakeFeeConfig(
chainStore,
chainStore.osmosis.chainId,
gasForecastedCollectAndReinvestRewards
);

const collectRewardsDisabled = summedStakeRewards
.toDec()
.lte(collectRewardsFee ? collectRewardsFee.toDec() : new Dec(0));

const collectAndReinvestRewardsDisabled = summedStakeRewards
.toDec()
.lte(
collectAndReinvestRewardsFee
? collectAndReinvestRewardsFee.toDec()
: new Dec(0)
);

const collectAndReinvestRewards = useCallback(() => {
logEvent([EventName.Stake.collectAndReinvestStarted]);

// if (account?.osmosis) {
// account.osmosis.collectAndReinvest_mock(
// "",
// (tx: DeliverTxResponse) => {
// if (tx.code === 0) {
// logEvent([EventName.Stake.collectAndReinvestStarted]);
// }
// }
// );
// }
}, [account, logEvent]);
const collectAndReinvestCoin: { amount: string; denom: Currency } = {
amount: osmoRewardsAmount,
denom: osmo,
};

if (account?.osmosis) {
account.osmosis.sendWithdrawDelegationRewardsAndSendDelegateToValidatorSetMsgs(
collectAndReinvestCoin,
"",
(tx: DeliverTxResponse) => {
if (tx.code === 0) {
logEvent([EventName.Stake.collectAndReinvestCompleted]);
}
}
);
}
}, [account, logEvent, osmo, osmoRewardsAmount]);

return (
<GenericMainCard title={t("stake.dashboard")} titleIcon={icon}>
<div className="flex w-full flex-row justify-between gap-4 py-10 sm:flex-col sm:py-4">
<StakeBalances
title={t("stake.stakeBalanceTitle")}
dollarAmount={String(fiatBalance)}
osmoAmount={balance.toString()}
dollarAmount={fiatBalance}
osmoAmount={balance}
/>
<StakeBalances
title={t("stake.rewardsTitle")}
dollarAmount={String(fiatRewards)}
osmoAmount={coinPrettyStakeRewards
.moveDecimalPointRight(osmo.coinDecimals)
.toString()}
dollarAmount={fiatRewards}
osmoAmount={summedStakeRewards}
/>
</div>
<ValidatorSquadCard
Expand All @@ -108,19 +141,21 @@ export const StakeDashboard: React.FC<{
/>
<div className="flex h-full max-h-[9.375rem] w-full flex-grow flex-row space-x-2">
<RewardsCard
disabled={collectRewardsDisabled}
title={t("stake.collectRewards")}
tooltipContent={t("stake.collectRewardsTooltip")}
disabledTooltipContent={t("stake.collectRewardsTooltipDisabled")}
onClick={collectRewards}
containerClasses="relative overflow-hidden"
image={
<div className="pointer-events-none absolute left-[-2.5rem] bottom-[-2.1875rem] h-full w-full bg-[url('/images/gift-box.svg')] bg-contain bg-no-repeat lg:invisible" />
}
/>
<RewardsCard
disabled={collectAndReinvestRewardsDisabled}
title={t("stake.investRewards")}
tooltipContent={t("stake.collectAndReinvestTooltip")}
disabledTooltipContent={t("stake.collectRewardsTooltipDisabled")}
onClick={collectAndReinvestRewards}
containerClasses="relative overflow-hidden"
image={
<div className="pointer-events-none absolute left-[-1.5625rem] bottom-[-2.1875rem] h-full w-full bg-[url('/images/piggy-bank.svg')] bg-contain bg-no-repeat lg:invisible" />
}
Expand All @@ -133,18 +168,17 @@ export const StakeDashboard: React.FC<{

const StakeBalances: React.FC<{
title: string;
dollarAmount: string;
osmoAmount?: string;
dollarAmount?: PricePretty;
osmoAmount?: CoinPretty;
}> = ({ title, dollarAmount, osmoAmount }) => {
return (
<div className="flex w-full flex-col items-center justify-center text-left">
{/* <div> */}
<span className="caption text-sm text-osmoverse-200 md:text-xs">
{title}
</span>
<h3 className="whitespace-nowrap">{dollarAmount}</h3>
<h3 className="whitespace-nowrap">{dollarAmount?.toString() ?? ""}</h3>
<span className="caption text-sm text-osmoverse-200 md:text-xs">
{osmoAmount}
{osmoAmount?.toString() ?? ""}
</span>
</div>
);
Expand Down
1 change: 1 addition & 0 deletions packages/web/localizations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,7 @@
"unbondingPeriodTooltip": "Locked tokens increase stability for the blockchain. When you unstake your tokens, they will be available after the specified unbonding period.",
"estimatedEarningsTooltip": "These earning are calculated based on the current staking APR, which is subject to change.",
"collectRewardsTooltip": "Collect all your unclaimed rewards. These tokens will be immediately available to you on Osmosis.",
"collectRewardsTooltipDisabled": "Your rewards are too low to collect, please try again once you have more rewards to cover gas costs.",
"collectAndReinvestTooltip": "Collect and automatically re-stake your unclaimed earnings in one click. Compounding is a great way to earn more with your assets.",
"isAPRTooHighTooltip": "This validator takes an unusually high commission. Consider selecting a different validator.",
"isVotingPowerTooHighTooltip": "This is a top 10 validator with high voting power. Consider selecting a different validator to help promote decentralization.",
Expand Down

0 comments on commit fe36573

Please sign in to comment.