Skip to content

Commit 622044d

Browse files
feat: shielded rewards intergration (#1378)
* feat: shielded rewards intergration * fix: shielded rewards call * fix: undefined TextDecoder * fix: show shielded rewards every epoch * fix: unit tests * feat: shielded rewards estimation
1 parent a1cb27a commit 622044d

File tree

8 files changed

+188
-8
lines changed

8 files changed

+188
-8
lines changed

apps/namadillo/src/App/AccountOverview/NamBalanceContainer.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Heading, SkeletonLoading, Stack } from "@namada/components";
22
import { AtomErrorBoundary } from "App/Common/AtomErrorBoundary";
33
import { NamCurrency } from "App/Common/NamCurrency";
44
import { ShieldedRewardsBox } from "App/Masp/ShieldedRewardsBox";
5+
import { cachedShieldedRewardsAtom } from "atoms/balance";
56
import { applicationFeaturesAtom } from "atoms/settings";
67
import BigNumber from "bignumber.js";
78
import clsx from "clsx";
@@ -82,6 +83,7 @@ export const NamBalanceContainer = (): JSX.Element => {
8283
const { maspEnabled, shieldingRewardsEnabled } = useAtomValue(
8384
applicationFeaturesAtom
8485
);
86+
const shieldedRewards = useAtomValue(cachedShieldedRewardsAtom);
8587

8688
const {
8789
balanceQuery,
@@ -145,7 +147,9 @@ export const NamBalanceContainer = (): JSX.Element => {
145147
isEnabled={shieldingRewardsEnabled}
146148
className="flex flex-1"
147149
>
148-
<ShieldedRewardsBox />
150+
<ShieldedRewardsBox
151+
shieldedRewardsAmount={shieldedRewards.amount}
152+
/>
149153
</ListItemContainer>
150154
</Stack>
151155
</AtomErrorBoundary>

apps/namadillo/src/App/AccountOverview/__tests__/NamBalanceContainer.test.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { cleanup, render, screen } from "@testing-library/react";
22
import BigNumber from "bignumber.js";
33
import { mockUseBalances } from "hooks/__mocks__/mockUseBalance";
4+
import { atom } from "jotai";
45
import { AtomWithQueryResult } from "jotai-tanstack-query";
56
import { NamBalanceContainer } from "../NamBalanceContainer";
67

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

12+
jest.mock("atoms/balance", () => ({
13+
cachedShieldedRewardsAtom: atom({ data: undefined }),
14+
}));
15+
1116
describe("Component: NamBalanceContainer", () => {
1217
beforeEach(() => {
1318
jest.clearAllMocks();

apps/namadillo/src/App/Masp/ShieldedNamBalance.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { SkeletonLoading, Stack, Tooltip } from "@namada/components";
22
import { AtomErrorBoundary } from "App/Common/AtomErrorBoundary";
33
import { NamCurrency } from "App/Common/NamCurrency";
4-
import { shieldedTokensAtom } from "atoms/balance/atoms";
4+
import {
5+
cachedShieldedRewardsAtom,
6+
shieldedTokensAtom,
7+
} from "atoms/balance/atoms";
58
import { getTotalNam } from "atoms/balance/functions";
69
import { applicationFeaturesAtom } from "atoms/settings/atoms";
710
import BigNumber from "bignumber.js";
@@ -29,7 +32,7 @@ const AsyncNamCurrency = ({
2932

3033
return (
3134
<NamCurrency
32-
amount={new BigNumber(amount)}
35+
amount={amount}
3336
className={twMerge("block text-center text-3xl leading-none", className)}
3437
currencySymbolClassName="block text-xs mt-1"
3538
/>
@@ -39,6 +42,7 @@ const AsyncNamCurrency = ({
3942
export const ShieldedNamBalance = (): JSX.Element => {
4043
const shieldedTokensQuery = useAtomValue(shieldedTokensAtom);
4144
const { shieldingRewardsEnabled } = useAtomValue(applicationFeaturesAtom);
45+
const shieldedRewards = useAtomValue(cachedShieldedRewardsAtom);
4246

4347
const shieldedNam =
4448
shieldedTokensQuery.isPending ? undefined : (
@@ -109,8 +113,7 @@ export const ShieldedNamBalance = (): JSX.Element => {
109113
rewards per Epoch
110114
</div>
111115
{shieldingRewardsEnabled ?
112-
// TODO shielding rewards
113-
<AsyncNamCurrency amount={new BigNumber(0)} />
116+
<AsyncNamCurrency amount={shieldedRewards.amount} />
114117
: <div className="block text-center text-3xl">--</div>}
115118
<div
116119
className={twMerge(

apps/namadillo/src/App/Settings/SettingsMASP.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { routes } from "App/routes";
33
import {
44
lastCompletedShieldedSyncAtom,
55
storageShieldedBalanceAtom,
6+
storageShieldedRewardsAtom,
67
} from "atoms/balance/atoms";
78
import { clearShieldedContextAtom } from "atoms/settings";
89
import { useAtom, useSetAtom } from "jotai";
@@ -14,11 +15,13 @@ export const SettingsMASP = (): JSX.Element => {
1415
const setLastCompletedShieldedSync = useSetAtom(
1516
lastCompletedShieldedSyncAtom
1617
);
18+
const setStorageShieldedRewards = useSetAtom(storageShieldedRewardsAtom);
1719

1820
const onInvalidateShieldedContext = async (): Promise<void> => {
1921
await clearShieldedContext.mutateAsync();
2022
setStorageShieldedBalance(RESET);
2123
setLastCompletedShieldedSync(undefined);
24+
setStorageShieldedRewards(RESET);
2225
location.href = routes.root;
2326
};
2427

apps/namadillo/src/atoms/balance/atoms.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,15 @@ import { atom, getDefaultStore } from "jotai";
2121
import { atomWithQuery } from "jotai-tanstack-query";
2222
import { atomWithStorage } from "jotai/utils";
2323
import { Address, AddressWithAsset } from "types";
24+
import { namadaAsset, toDisplayAmount } from "utils";
2425
import {
2526
mapNamadaAddressesToAssets,
2627
mapNamadaAssetsToTokenBalances,
2728
} from "./functions";
2829
import {
2930
fetchBlockHeightByTimestamp,
3031
fetchShieldedBalance,
32+
fetchShieldRewards,
3133
shieldedSync,
3234
} from "./services";
3335

@@ -282,3 +284,49 @@ export const transparentTokensAtom = atomWithQuery<TokenBalance[]>((get) => {
282284
),
283285
};
284286
});
287+
288+
export const storageShieldedRewardsAtom = atomWithStorage<
289+
Record<Address, { minDenomAmount: string }>
290+
>("namadillo:shieldedRewards", {});
291+
292+
export const shieldRewardsAtom = atomWithQuery((get) => {
293+
const viewingKeysQuery = get(viewingKeysAtom);
294+
const chainParametersQuery = get(chainParametersAtom);
295+
const { set } = getDefaultStore();
296+
297+
return {
298+
queryKey: ["shield-rewards", viewingKeysQuery.data],
299+
...queryDependentFn(async () => {
300+
const [viewingKey] = viewingKeysQuery.data!;
301+
const { chainId } = chainParametersQuery.data!;
302+
const minDenomAmount = BigNumber(
303+
await fetchShieldRewards(viewingKey, chainId)
304+
);
305+
306+
const storage = get(storageShieldedRewardsAtom);
307+
set(storageShieldedRewardsAtom, {
308+
...storage,
309+
[viewingKey.key]: { minDenomAmount: minDenomAmount.toString() },
310+
});
311+
312+
return { minDenomAmount };
313+
}, [viewingKeysQuery, chainParametersQuery]),
314+
};
315+
});
316+
317+
export const cachedShieldedRewardsAtom = atom((get) => {
318+
const viewingKeysQuery = get(viewingKeysAtom);
319+
const storage = get(storageShieldedRewardsAtom);
320+
321+
if (!viewingKeysQuery.data || !storage) {
322+
return { amount: BigNumber(0) };
323+
}
324+
const [viewingKey] = viewingKeysQuery.data;
325+
326+
const rewards = get(shieldRewardsAtom);
327+
const data = rewards.isSuccess ? rewards.data : storage[viewingKey.key];
328+
329+
return {
330+
amount: toDisplayAmount(namadaAsset(), BigNumber(data.minDenomAmount)),
331+
};
332+
});

apps/namadillo/src/atoms/balance/services.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,22 @@ export const fetchBlockHeightByTimestamp = async (
104104

105105
return Number(response.data.height);
106106
};
107+
108+
export const fetchShieldRewards = async (
109+
viewingKey: DatedViewingKey,
110+
chainId: string
111+
): Promise<string> => {
112+
const sdk = await getSdkInstance();
113+
114+
return await sdk.rpc.shieldedRewards(viewingKey.key, chainId);
115+
};
116+
117+
export const simulateRewardPerToken = async (
118+
chainId: string,
119+
token: string,
120+
amount: string
121+
): Promise<string> => {
122+
const sdk = await getSdkInstance();
123+
124+
return await sdk.rpc.simulateShieldedRewards(chainId, token, amount);
125+
};

packages/sdk/src/rpc/rpc.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,4 +253,30 @@ export class Rpc {
253253

254254
await this.query.shielded_sync(datedViewingKeys, chainId);
255255
}
256+
257+
/**
258+
* Return shielded rewards for specific owner for next epoch
259+
* @async
260+
* @param owner - Viewing key of an owner
261+
* @param chainId - Chain ID to load the context for
262+
* @returns amount in base units
263+
*/
264+
async shieldedRewards(owner: string, chainId: string): Promise<string> {
265+
return await this.sdk.shielded_rewards(owner, chainId);
266+
}
267+
268+
/**
269+
* Simulate shielded rewards per token and amount in next epoch
270+
* @param chainId - Chain ID to load the context for
271+
* @param token - Token address
272+
* @param amount - Denominated amount
273+
* @returns amount in base units
274+
*/
275+
async simulateShieldedRewards(
276+
chainId: string,
277+
token: string,
278+
amount: string
279+
): Promise<string> {
280+
return await this.sdk.simulate_shielded_rewards(chainId, token, amount);
281+
}
256282
}

packages/shared/lib/src/sdk/mod.rs

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,18 @@ use namada_sdk::ibc::convert_masp_tx_to_ibc_memo;
2626
use namada_sdk::ibc::core::host::types::identifiers::{ChannelId, PortId};
2727
use namada_sdk::io::NamadaIo;
2828
use namada_sdk::key::{common, ed25519, RefTo, SigScheme};
29+
use namada_sdk::masp::shielded_wallet::ShieldedApi;
2930
use namada_sdk::masp::ShieldedContext;
30-
use namada_sdk::masp_primitives::transaction::components::sapling::fees::InputView;
31+
use namada_sdk::masp_primitives::transaction::components::{
32+
amount::I128Sum, sapling::fees::InputView,
33+
};
3134
use namada_sdk::masp_primitives::zip32::{ExtendedFullViewingKey, ExtendedKey};
35+
use namada_sdk::rpc::query_denom;
3236
use namada_sdk::rpc::{query_epoch, InnerTxResult};
3337
use namada_sdk::signing::SigningTxData;
3438
use namada_sdk::string_encoding::Format;
3539
use namada_sdk::tendermint_rpc::Url;
36-
use namada_sdk::token::DenominatedAmount;
40+
use namada_sdk::token::{Amount, DenominatedAmount, MaspEpoch};
3741
use namada_sdk::token::{MaspTxId, OptionExt};
3842
use namada_sdk::tx::data::TxType;
3943
use namada_sdk::tx::{
@@ -44,7 +48,7 @@ use namada_sdk::tx::{
4448
ProcessTxResponse, Tx,
4549
};
4650
use namada_sdk::wallet::{Store, Wallet};
47-
use namada_sdk::{Namada, NamadaImpl, PaymentAddress, TransferTarget};
51+
use namada_sdk::{ExtendedViewingKey, Namada, NamadaImpl, PaymentAddress, TransferTarget};
4852
use std::collections::BTreeMap;
4953
use std::str::FromStr;
5054
use tx::MaspSigningData;
@@ -760,6 +764,74 @@ impl Sdk {
760764
}
761765
}
762766

767+
// This should be a part of query.rs but we have to pass whole "namada" into estimate_next_epoch_rewards
768+
pub async fn shielded_rewards(
769+
&self,
770+
owner: String,
771+
chain_id: String,
772+
) -> Result<JsValue, JsError> {
773+
let mut shielded: ShieldedContext<masp::JSShieldedUtils> = ShieldedContext::default();
774+
shielded.utils.chain_id = chain_id.clone();
775+
shielded.load().await?;
776+
777+
let xvk = ExtendedViewingKey::from_str(&owner)?;
778+
let raw_balance = shielded
779+
.compute_shielded_balance(&xvk.as_viewing_key())
780+
.await
781+
.map_err(|e| JsError::new(&e.to_string()))?;
782+
783+
let rewards = match raw_balance {
784+
Some(balance) => shielded
785+
.estimate_next_epoch_rewards(&self.namada, &balance)
786+
.await
787+
.map(|r| r.amount())
788+
.map_err(|e| JsError::new(&e.to_string()))?,
789+
None => Amount::zero(),
790+
};
791+
792+
to_js_result(rewards.to_string())
793+
}
794+
795+
pub async fn simulate_shielded_rewards(
796+
&self,
797+
chain_id: String,
798+
token: String,
799+
amount: String,
800+
) -> Result<JsValue, JsError> {
801+
let token = Address::from_str(&token)?;
802+
// TODO: as an improvement we could pass the denom from the client
803+
let denom = query_denom(&self.namada.client, &token)
804+
.await
805+
.ok_or(JsError::new(&format!(
806+
"Denom for token {} not found",
807+
token.to_string()
808+
)))?;
809+
let amount = DenominatedAmount::new(Amount::from_str(amount, denom)?, denom);
810+
811+
let mut shielded: ShieldedContext<masp::JSShieldedUtils> = ShieldedContext::default();
812+
shielded.utils.chain_id = chain_id.clone();
813+
shielded.load().await?;
814+
815+
let (_, masp_value) = shielded
816+
.convert_namada_amount_to_masp(
817+
self.namada.client(),
818+
// Masp epoch should not matter
819+
MaspEpoch::zero(),
820+
&token,
821+
amount.denom(),
822+
amount.amount(),
823+
)
824+
.await
825+
.map_err(|e| JsError::new(&e.to_string()))?;
826+
827+
let reward = shielded
828+
.estimate_next_epoch_rewards(&self.namada, &I128Sum::from_sum(masp_value.clone()))
829+
.await
830+
.map_err(|e| JsError::new(&e.to_string()))?;
831+
832+
to_js_result(reward)
833+
}
834+
763835
pub fn masp_address(&self) -> String {
764836
MASP.to_string()
765837
}

0 commit comments

Comments
 (0)