Skip to content

Commit

Permalink
feat: shielded rewards intergration (#1378)
Browse files Browse the repository at this point in the history
* feat: shielded rewards intergration

* fix: shielded rewards call

* fix: undefined TextDecoder

* fix: show shielded rewards every epoch

* fix: unit tests

* feat: shielded rewards estimation
  • Loading branch information
mateuszjasiuk authored Jan 31, 2025
1 parent a1cb27a commit 622044d
Show file tree
Hide file tree
Showing 8 changed files with 188 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Heading, SkeletonLoading, Stack } from "@namada/components";
import { AtomErrorBoundary } from "App/Common/AtomErrorBoundary";
import { NamCurrency } from "App/Common/NamCurrency";
import { ShieldedRewardsBox } from "App/Masp/ShieldedRewardsBox";
import { cachedShieldedRewardsAtom } from "atoms/balance";
import { applicationFeaturesAtom } from "atoms/settings";
import BigNumber from "bignumber.js";
import clsx from "clsx";
Expand Down Expand Up @@ -82,6 +83,7 @@ export const NamBalanceContainer = (): JSX.Element => {
const { maspEnabled, shieldingRewardsEnabled } = useAtomValue(
applicationFeaturesAtom
);
const shieldedRewards = useAtomValue(cachedShieldedRewardsAtom);

const {
balanceQuery,
Expand Down Expand Up @@ -145,7 +147,9 @@ export const NamBalanceContainer = (): JSX.Element => {
isEnabled={shieldingRewardsEnabled}
className="flex flex-1"
>
<ShieldedRewardsBox />
<ShieldedRewardsBox
shieldedRewardsAmount={shieldedRewards.amount}
/>
</ListItemContainer>
</Stack>
</AtomErrorBoundary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { cleanup, render, screen } from "@testing-library/react";
import BigNumber from "bignumber.js";
import { mockUseBalances } from "hooks/__mocks__/mockUseBalance";
import { atom } from "jotai";
import { AtomWithQueryResult } from "jotai-tanstack-query";
import { NamBalanceContainer } from "../NamBalanceContainer";

jest.mock("hooks/useBalances", () => ({
useBalances: jest.fn(),
}));

jest.mock("atoms/balance", () => ({
cachedShieldedRewardsAtom: atom({ data: undefined }),
}));

describe("Component: NamBalanceContainer", () => {
beforeEach(() => {
jest.clearAllMocks();
Expand Down
11 changes: 7 additions & 4 deletions apps/namadillo/src/App/Masp/ShieldedNamBalance.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { SkeletonLoading, Stack, Tooltip } from "@namada/components";
import { AtomErrorBoundary } from "App/Common/AtomErrorBoundary";
import { NamCurrency } from "App/Common/NamCurrency";
import { shieldedTokensAtom } from "atoms/balance/atoms";
import {
cachedShieldedRewardsAtom,
shieldedTokensAtom,
} from "atoms/balance/atoms";
import { getTotalNam } from "atoms/balance/functions";
import { applicationFeaturesAtom } from "atoms/settings/atoms";
import BigNumber from "bignumber.js";
Expand Down Expand Up @@ -29,7 +32,7 @@ const AsyncNamCurrency = ({

return (
<NamCurrency
amount={new BigNumber(amount)}
amount={amount}
className={twMerge("block text-center text-3xl leading-none", className)}
currencySymbolClassName="block text-xs mt-1"
/>
Expand All @@ -39,6 +42,7 @@ const AsyncNamCurrency = ({
export const ShieldedNamBalance = (): JSX.Element => {
const shieldedTokensQuery = useAtomValue(shieldedTokensAtom);
const { shieldingRewardsEnabled } = useAtomValue(applicationFeaturesAtom);
const shieldedRewards = useAtomValue(cachedShieldedRewardsAtom);

const shieldedNam =
shieldedTokensQuery.isPending ? undefined : (
Expand Down Expand Up @@ -109,8 +113,7 @@ export const ShieldedNamBalance = (): JSX.Element => {
rewards per Epoch
</div>
{shieldingRewardsEnabled ?
// TODO shielding rewards
<AsyncNamCurrency amount={new BigNumber(0)} />
<AsyncNamCurrency amount={shieldedRewards.amount} />
: <div className="block text-center text-3xl">--</div>}
<div
className={twMerge(
Expand Down
3 changes: 3 additions & 0 deletions apps/namadillo/src/App/Settings/SettingsMASP.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { routes } from "App/routes";
import {
lastCompletedShieldedSyncAtom,
storageShieldedBalanceAtom,
storageShieldedRewardsAtom,
} from "atoms/balance/atoms";
import { clearShieldedContextAtom } from "atoms/settings";
import { useAtom, useSetAtom } from "jotai";
Expand All @@ -14,11 +15,13 @@ export const SettingsMASP = (): JSX.Element => {
const setLastCompletedShieldedSync = useSetAtom(
lastCompletedShieldedSyncAtom
);
const setStorageShieldedRewards = useSetAtom(storageShieldedRewardsAtom);

const onInvalidateShieldedContext = async (): Promise<void> => {
await clearShieldedContext.mutateAsync();
setStorageShieldedBalance(RESET);
setLastCompletedShieldedSync(undefined);
setStorageShieldedRewards(RESET);
location.href = routes.root;
};

Expand Down
48 changes: 48 additions & 0 deletions apps/namadillo/src/atoms/balance/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ import { atom, getDefaultStore } from "jotai";
import { atomWithQuery } from "jotai-tanstack-query";
import { atomWithStorage } from "jotai/utils";
import { Address, AddressWithAsset } from "types";
import { namadaAsset, toDisplayAmount } from "utils";
import {
mapNamadaAddressesToAssets,
mapNamadaAssetsToTokenBalances,
} from "./functions";
import {
fetchBlockHeightByTimestamp,
fetchShieldedBalance,
fetchShieldRewards,
shieldedSync,
} from "./services";

Expand Down Expand Up @@ -282,3 +284,49 @@ export const transparentTokensAtom = atomWithQuery<TokenBalance[]>((get) => {
),
};
});

export const storageShieldedRewardsAtom = atomWithStorage<
Record<Address, { minDenomAmount: string }>
>("namadillo:shieldedRewards", {});

export const shieldRewardsAtom = atomWithQuery((get) => {
const viewingKeysQuery = get(viewingKeysAtom);
const chainParametersQuery = get(chainParametersAtom);
const { set } = getDefaultStore();

return {
queryKey: ["shield-rewards", viewingKeysQuery.data],
...queryDependentFn(async () => {
const [viewingKey] = viewingKeysQuery.data!;
const { chainId } = chainParametersQuery.data!;
const minDenomAmount = BigNumber(
await fetchShieldRewards(viewingKey, chainId)
);

const storage = get(storageShieldedRewardsAtom);
set(storageShieldedRewardsAtom, {
...storage,
[viewingKey.key]: { minDenomAmount: minDenomAmount.toString() },
});

return { minDenomAmount };
}, [viewingKeysQuery, chainParametersQuery]),
};
});

export const cachedShieldedRewardsAtom = atom((get) => {
const viewingKeysQuery = get(viewingKeysAtom);
const storage = get(storageShieldedRewardsAtom);

if (!viewingKeysQuery.data || !storage) {
return { amount: BigNumber(0) };
}
const [viewingKey] = viewingKeysQuery.data;

const rewards = get(shieldRewardsAtom);
const data = rewards.isSuccess ? rewards.data : storage[viewingKey.key];

return {
amount: toDisplayAmount(namadaAsset(), BigNumber(data.minDenomAmount)),
};
});
19 changes: 19 additions & 0 deletions apps/namadillo/src/atoms/balance/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,22 @@ export const fetchBlockHeightByTimestamp = async (

return Number(response.data.height);
};

export const fetchShieldRewards = async (
viewingKey: DatedViewingKey,
chainId: string
): Promise<string> => {
const sdk = await getSdkInstance();

return await sdk.rpc.shieldedRewards(viewingKey.key, chainId);
};

export const simulateRewardPerToken = async (
chainId: string,
token: string,
amount: string
): Promise<string> => {
const sdk = await getSdkInstance();

return await sdk.rpc.simulateShieldedRewards(chainId, token, amount);
};
26 changes: 26 additions & 0 deletions packages/sdk/src/rpc/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,4 +253,30 @@ export class Rpc {

await this.query.shielded_sync(datedViewingKeys, chainId);
}

/**
* Return shielded rewards for specific owner for next epoch
* @async
* @param owner - Viewing key of an owner
* @param chainId - Chain ID to load the context for
* @returns amount in base units
*/
async shieldedRewards(owner: string, chainId: string): Promise<string> {
return await this.sdk.shielded_rewards(owner, chainId);
}

/**
* Simulate shielded rewards per token and amount in next epoch
* @param chainId - Chain ID to load the context for
* @param token - Token address
* @param amount - Denominated amount
* @returns amount in base units
*/
async simulateShieldedRewards(
chainId: string,
token: string,
amount: string
): Promise<string> {
return await this.sdk.simulate_shielded_rewards(chainId, token, amount);
}
}
78 changes: 75 additions & 3 deletions packages/shared/lib/src/sdk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,18 @@ use namada_sdk::ibc::convert_masp_tx_to_ibc_memo;
use namada_sdk::ibc::core::host::types::identifiers::{ChannelId, PortId};
use namada_sdk::io::NamadaIo;
use namada_sdk::key::{common, ed25519, RefTo, SigScheme};
use namada_sdk::masp::shielded_wallet::ShieldedApi;
use namada_sdk::masp::ShieldedContext;
use namada_sdk::masp_primitives::transaction::components::sapling::fees::InputView;
use namada_sdk::masp_primitives::transaction::components::{
amount::I128Sum, sapling::fees::InputView,
};
use namada_sdk::masp_primitives::zip32::{ExtendedFullViewingKey, ExtendedKey};
use namada_sdk::rpc::query_denom;
use namada_sdk::rpc::{query_epoch, InnerTxResult};
use namada_sdk::signing::SigningTxData;
use namada_sdk::string_encoding::Format;
use namada_sdk::tendermint_rpc::Url;
use namada_sdk::token::DenominatedAmount;
use namada_sdk::token::{Amount, DenominatedAmount, MaspEpoch};
use namada_sdk::token::{MaspTxId, OptionExt};
use namada_sdk::tx::data::TxType;
use namada_sdk::tx::{
Expand All @@ -44,7 +48,7 @@ use namada_sdk::tx::{
ProcessTxResponse, Tx,
};
use namada_sdk::wallet::{Store, Wallet};
use namada_sdk::{Namada, NamadaImpl, PaymentAddress, TransferTarget};
use namada_sdk::{ExtendedViewingKey, Namada, NamadaImpl, PaymentAddress, TransferTarget};
use std::collections::BTreeMap;
use std::str::FromStr;
use tx::MaspSigningData;
Expand Down Expand Up @@ -760,6 +764,74 @@ impl Sdk {
}
}

// This should be a part of query.rs but we have to pass whole "namada" into estimate_next_epoch_rewards
pub async fn shielded_rewards(
&self,
owner: String,
chain_id: String,
) -> Result<JsValue, JsError> {
let mut shielded: ShieldedContext<masp::JSShieldedUtils> = ShieldedContext::default();
shielded.utils.chain_id = chain_id.clone();
shielded.load().await?;

let xvk = ExtendedViewingKey::from_str(&owner)?;
let raw_balance = shielded
.compute_shielded_balance(&xvk.as_viewing_key())
.await
.map_err(|e| JsError::new(&e.to_string()))?;

let rewards = match raw_balance {
Some(balance) => shielded
.estimate_next_epoch_rewards(&self.namada, &balance)
.await
.map(|r| r.amount())
.map_err(|e| JsError::new(&e.to_string()))?,
None => Amount::zero(),
};

to_js_result(rewards.to_string())
}

pub async fn simulate_shielded_rewards(
&self,
chain_id: String,
token: String,
amount: String,
) -> Result<JsValue, JsError> {
let token = Address::from_str(&token)?;
// TODO: as an improvement we could pass the denom from the client
let denom = query_denom(&self.namada.client, &token)
.await
.ok_or(JsError::new(&format!(
"Denom for token {} not found",
token.to_string()
)))?;
let amount = DenominatedAmount::new(Amount::from_str(amount, denom)?, denom);

let mut shielded: ShieldedContext<masp::JSShieldedUtils> = ShieldedContext::default();
shielded.utils.chain_id = chain_id.clone();
shielded.load().await?;

let (_, masp_value) = shielded
.convert_namada_amount_to_masp(
self.namada.client(),
// Masp epoch should not matter
MaspEpoch::zero(),
&token,
amount.denom(),
amount.amount(),
)
.await
.map_err(|e| JsError::new(&e.to_string()))?;

let reward = shielded
.estimate_next_epoch_rewards(&self.namada, &I128Sum::from_sum(masp_value.clone()))
.await
.map_err(|e| JsError::new(&e.to_string()))?;

to_js_result(reward)
}

pub fn masp_address(&self) -> String {
MASP.to_string()
}
Expand Down

0 comments on commit 622044d

Please sign in to comment.