diff --git a/apps/namadillo/src/App/AppRoutes.tsx b/apps/namadillo/src/App/AppRoutes.tsx index 932af40c42..5367cdb823 100644 --- a/apps/namadillo/src/App/AppRoutes.tsx +++ b/apps/namadillo/src/App/AppRoutes.tsx @@ -31,6 +31,7 @@ import { routes } from "./routes"; import { Advanced } from "./Settings/Advanced"; import { EnableFeatures } from "./Settings/EnableFeatures"; import { SettingsMain } from "./Settings/SettingsMain"; +import { SettingsMASP } from "./Settings/SettingsMASP"; import { SettingsPanel } from "./Settings/SettingsPanel"; import { SettingsSignArbitrary } from "./Settings/SettingsSignArbitrary"; import { SignMessages } from "./SignMessages/SignMessages"; @@ -144,6 +145,7 @@ export const MainRoutes = (): JSX.Element => { path={routes.settingsSignArbitrary} element={} /> + } /> } diff --git a/apps/namadillo/src/App/Masp/ShieldedBalanceChart.tsx b/apps/namadillo/src/App/Masp/ShieldedBalanceChart.tsx index 0e328646ed..b6a654637d 100644 --- a/apps/namadillo/src/App/Masp/ShieldedBalanceChart.tsx +++ b/apps/namadillo/src/App/Masp/ShieldedBalanceChart.tsx @@ -17,7 +17,6 @@ export const ShieldedBalanceChart = (): JSX.Element => { const shieldedTokensQuery = useAtomValue(shieldedTokensAtom); const [{ data: shieldedSyncProgress, refetch: shieledSync }] = useAtom(shieldedSyncAtom); - const [showSyncProgress, setShowSyncProgress] = useState(false); const [progress, setProgress] = useState({ [ProgressBarNames.Scanned]: 0, @@ -27,26 +26,37 @@ export const ShieldedBalanceChart = (): JSX.Element => { useEffect(() => { if (!shieldedSyncProgress) return; - const onProgressBarIncremented = ({ name, current, total, }: ProgressBarIncremented): void => { - const perc = - total === 0 ? 0 : Math.min(Math.floor((current / total) * 100), 100); + if (name === ProgressBarNames.Fetched) { + // TODO: this maybe can be improved by passing total in ProgressBarStarted event + // If total is more than one batch of 100, show progress + if (total > 100) { + setShowSyncProgress(true); + } - setProgress((prev) => ({ - ...prev, - [name]: perc, - })); + const perc = + total === 0 ? 0 : Math.min(Math.floor((current / total) * 100), 100); + + setProgress((prev) => ({ + ...prev, + [name]: perc, + })); + } }; const onProgressBarFinished = ({ name }: ProgressBarFinished): void => { - setProgress((prev) => ({ - ...prev, - [name]: 100, - })); + if (name === ProgressBarNames.Fetched) { + setProgress((prev) => ({ + ...prev, + [name]: 100, + })); + + setShowSyncProgress(false); + } }; shieldedSyncProgress.on( @@ -73,13 +83,6 @@ export const ShieldedBalanceChart = (): JSX.Element => { useEffect(() => { shieledSync(); - - const timeoutId = setTimeout(() => { - setShowSyncProgress(true); - }, 3000); - return () => { - clearTimeout(timeoutId); - }; }, []); const shieldedDollars = getTotalDollar(shieldedTokensQuery.data); @@ -91,7 +94,7 @@ export const ShieldedBalanceChart = (): JSX.Element => { result={shieldedTokensQuery} niceError="Unable to load balance" > - {shieldedTokensQuery.isPending ? + {shieldedTokensQuery.isPending || showSyncProgress ? { + const navigate = useNavigate(); + const [clearShieldedContext] = useAtom(clearShieldedContextAtom); + const [{ refetch: shieldedSync }] = useAtom(shieldedSyncAtom); + + const onInvalidateShieldedContext = async (): Promise => { + await clearShieldedContext.mutateAsync(); + shieldedSync(); + navigate(routes.masp); + }; + + return ( + +

Invalidate Shielded Context

+

+ In case your shielded balance is not updating correctly, you can + invalidate the shielded context to force a rescan. This might take a few + minutes to complete. +

+ + + Invalidate Shielded Context + +
+ ); +}; diff --git a/apps/namadillo/src/App/Settings/SettingsMain.tsx b/apps/namadillo/src/App/Settings/SettingsMain.tsx index 342848fe29..aff3c7c55c 100644 --- a/apps/namadillo/src/App/Settings/SettingsMain.tsx +++ b/apps/namadillo/src/App/Settings/SettingsMain.tsx @@ -15,6 +15,7 @@ export const SettingsMain = (): JSX.Element => { url={routes.settingsSignArbitrary} text="Sign Arbitrary" /> +
Namadillo Version: {version}
diff --git a/apps/namadillo/src/App/WorkerTest.tsx b/apps/namadillo/src/App/WorkerTest.tsx index 7a26ea3a00..a6ac29d6dd 100644 --- a/apps/namadillo/src/App/WorkerTest.tsx +++ b/apps/namadillo/src/App/WorkerTest.tsx @@ -54,6 +54,7 @@ export function WorkerTest(): JSX.Element { type: "sync", payload: { vks: [{ key: vk, birthday: 0 }], + chainId: chain!.id, }, }); diff --git a/apps/namadillo/src/App/routes.ts b/apps/namadillo/src/App/routes.ts index cf4519a6a8..611fb251c6 100644 --- a/apps/namadillo/src/App/routes.ts +++ b/apps/namadillo/src/App/routes.ts @@ -33,6 +33,7 @@ export const routes = { settings: "/settings", settingsAdvanced: "/settings/advanced", settingsSignArbitrary: "/settings/sign-arbitrary", + settingsMASP: "/settings/masp", settingsFeatures: "/settings/features", // Other diff --git a/apps/namadillo/src/atoms/balance/atoms.ts b/apps/namadillo/src/atoms/balance/atoms.ts index a07c06d11e..f4f387591f 100644 --- a/apps/namadillo/src/atoms/balance/atoms.ts +++ b/apps/namadillo/src/atoms/balance/atoms.ts @@ -110,6 +110,7 @@ export const shieldedSyncAtom = atomWithQuery( const namTokenAddressQuery = get(nativeTokenAddressAtom); const rpcUrl = get(rpcUrlAtom); const maspIndexerUrl = get(maspIndexerUrlAtom); + const parametersQuery = get(chainParametersAtom); return { queryKey: [ @@ -122,7 +123,8 @@ export const shieldedSyncAtom = atomWithQuery( ...queryDependentFn(async () => { const viewingKeys = viewingKeysQuery.data; const namTokenAddress = namTokenAddressQuery.data; - if (!namTokenAddress || !viewingKeys) { + const parameters = parametersQuery.data; + if (!namTokenAddress || !viewingKeys || !parameters) { return null; } const [_, allViewingKeys] = viewingKeys; @@ -130,9 +132,10 @@ export const shieldedSyncAtom = atomWithQuery( rpcUrl, maspIndexerUrl, namTokenAddress, - allViewingKeys + allViewingKeys, + parameters.chainId ); - }, [viewingKeysQuery, namTokenAddressQuery]), + }, [viewingKeysQuery, namTokenAddressQuery, parametersQuery]), }; } ); @@ -147,6 +150,7 @@ export const shieldedBalanceAtom = atomWithQuery< const rpcUrl = get(rpcUrlAtom); const maspIndexerUrl = get(maspIndexerUrlAtom); const shieldedSync = get(shieldedSyncAtom); + const chainParametersQuery = get(chainParametersAtom); return { refetchInterval: enablePolling ? 1000 : false, @@ -163,7 +167,8 @@ export const shieldedBalanceAtom = atomWithQuery< const viewingKeys = viewingKeysQuery.data; const chainTokens = chainTokensQuery.data; const syncEmitter = shieldedSync.data; - if (!viewingKeys || !chainTokens || !syncEmitter) { + const chain = chainParametersQuery.data; + if (!viewingKeys || !chainTokens || !syncEmitter || !chain) { return []; } const [viewingKey] = viewingKeys; @@ -174,14 +179,20 @@ export const shieldedBalanceAtom = atomWithQuery< const response = await fetchShieldedBalance( viewingKey, - chainTokens.map((t) => t.address) + chainTokens.map((t) => t.address), + chain.chainId ); const shieldedBalance = response.map(([address, amount]) => ({ address, minDenomAmount: BigNumber(amount), })); return shieldedBalance; - }, [viewingKeysQuery, chainTokensQuery, namTokenAddressQuery]), + }, [ + viewingKeysQuery, + chainTokensQuery, + namTokenAddressQuery, + chainParametersQuery, + ]), }; }); diff --git a/apps/namadillo/src/atoms/balance/services.ts b/apps/namadillo/src/atoms/balance/services.ts index 0dd37c1a21..2e33c6e109 100644 --- a/apps/namadillo/src/atoms/balance/services.ts +++ b/apps/namadillo/src/atoms/balance/services.ts @@ -29,7 +29,8 @@ export function shieldedSync( rpcUrl: string, maspIndexerUrl: string, token: string, - viewingKeys: DatedViewingKey[] + viewingKeys: DatedViewingKey[], + chainId: string ): EventEmitter { // Only one sync process at a time if (shieldedSyncEmitter) { @@ -63,7 +64,7 @@ export function shieldedSync( }); await shieldedSyncWorker.sync({ type: "sync", - payload: { vks: viewingKeys }, + payload: { vks: viewingKeys, chainId }, }); } finally { worker.terminate(); @@ -76,13 +77,14 @@ export function shieldedSync( export const fetchShieldedBalance = async ( viewingKey: DatedViewingKey, - addresses: string[] + addresses: string[], + chainId: string ): Promise => { // TODO mock shielded balance // return await mockShieldedBalance(viewingKey); const sdk = await getSdkInstance(); - return await sdk.rpc.queryBalance(viewingKey.key, addresses); + return await sdk.rpc.queryBalance(viewingKey.key, addresses, chainId); }; export const fetchBlockHeightByTimestamp = async ( @@ -93,16 +95,3 @@ export const fetchBlockHeightByTimestamp = async ( return Number(response.data.height); }; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const mockShieldedBalance = async ( - viewingKey: DatedViewingKey -): Promise => { - await new Promise((r) => setTimeout(() => r(0), 500)); - getSdkInstance().then((sdk) => sdk.rpc.shieldedSync([viewingKey])); - return [ - ["tnam1qy440ynh9fwrx8aewjvvmu38zxqgukgc259fzp6h", "37"], // nam - ["tnam1p5nnjnasjtfwen2kzg78fumwfs0eycqpecuc2jwz", "1"], // uatom - ["tnam1p4rm6gy30xzeehj29qr8v0t33xmwdlsn5ye0ezf0", "2"], // uosmo - ]; -}; diff --git a/apps/namadillo/src/atoms/chain/services.ts b/apps/namadillo/src/atoms/chain/services.ts index 34d1e406a9..c49a15115e 100644 --- a/apps/namadillo/src/atoms/chain/services.ts +++ b/apps/namadillo/src/atoms/chain/services.ts @@ -4,6 +4,7 @@ import { NativeToken, Parameters, } from "@namada/indexer-client"; +import { getSdkInstance } from "utils/sdk"; export const fetchRpcUrlFromIndexer = async ( api: DefaultApi @@ -24,3 +25,8 @@ export const fetchChainTokens = async ( ): Promise<(NativeToken | IbcToken)[]> => { return (await api.apiV1ChainTokenGet()).data; }; + +export const clearShieldedContext = async (chainId: string): Promise => { + const sdk = await getSdkInstance(); + await sdk.getMasp().clearShieldedContext(chainId); +}; diff --git a/apps/namadillo/src/atoms/settings/atoms.ts b/apps/namadillo/src/atoms/settings/atoms.ts index a93d165a85..41ed8294f8 100644 --- a/apps/namadillo/src/atoms/settings/atoms.ts +++ b/apps/namadillo/src/atoms/settings/atoms.ts @@ -1,10 +1,11 @@ import { isUrlValid, sanitizeUrl } from "@namada/utils"; -import { indexerRpcUrlAtom } from "atoms/chain"; +import { chainParametersAtom, indexerRpcUrlAtom } from "atoms/chain"; import { Getter, Setter, atom, getDefaultStore } from "jotai"; import { atomWithMutation, atomWithQuery } from "jotai-tanstack-query"; import { atomWithStorage } from "jotai/utils"; import { SettingsStorage } from "types"; import { + clearShieldedContext, fetchDefaultTomlConfig, isIndexerAlive, isMaspIndexerAlive, @@ -206,3 +207,15 @@ export const indexerHeartbeatAtom = atomWithQuery((get) => { }, }; }); + +export const clearShieldedContextAtom = atomWithMutation((get) => { + const parameters = get(chainParametersAtom); + if (!parameters.data) { + throw new Error("Chain parameters not loaded"); + } + + return { + mutationKey: ["clear-shielded-context"], + mutationFn: () => clearShieldedContext(parameters.data.chainId), + }; +}); diff --git a/apps/namadillo/src/atoms/settings/services.ts b/apps/namadillo/src/atoms/settings/services.ts index 5230ba0922..094a19b232 100644 --- a/apps/namadillo/src/atoms/settings/services.ts +++ b/apps/namadillo/src/atoms/settings/services.ts @@ -2,6 +2,7 @@ import { Configuration, DefaultApi } from "@namada/indexer-client"; import { isUrlValid } from "@namada/utils"; import toml from "toml"; import { SettingsTomlOptions } from "types"; +import { getSdkInstance } from "utils/sdk"; export const isIndexerAlive = async (url: string): Promise => { if (!isUrlValid(url)) { @@ -46,3 +47,8 @@ export const fetchDefaultTomlConfig = const response = await fetch("/config.toml"); return toml.parse(await response.text()) as SettingsTomlOptions; }; + +export const clearShieldedContext = async (chainId: string): Promise => { + const sdk = await getSdkInstance(); + await sdk.getMasp().clearShieldedContext(chainId); +}; diff --git a/apps/namadillo/src/hooks/useSdk.tsx b/apps/namadillo/src/hooks/useSdk.tsx index 0beeabc0e8..ffaba51f5e 100644 --- a/apps/namadillo/src/hooks/useSdk.tsx +++ b/apps/namadillo/src/hooks/useSdk.tsx @@ -1,6 +1,6 @@ import { Sdk } from "@namada/sdk/web"; import { QueryStatus, useQuery } from "@tanstack/react-query"; -import { nativeTokenAddressAtom } from "atoms/chain"; +import { chainParametersAtom, nativeTokenAddressAtom } from "atoms/chain"; import { useAtomValue } from "jotai"; import { createContext, @@ -29,20 +29,21 @@ export const SdkProvider: FunctionComponent = ({ }) => { const [sdk, setSdk] = useState(); const nativeToken = useAtomValue(nativeTokenAddressAtom); + const parameters = useAtomValue(chainParametersAtom); // fetchAndStoreMaspParams() returns nothing, // so we return boolean on success for the query to succeed: - const fetchMaspParams = async (): Promise => { + const fetchMaspParams = async (chainId: string): Promise => { const { masp } = sdk!; return masp.hasMaspParams().then(async (hasMaspParams) => { if (hasMaspParams) { - await masp.loadMaspParams("").catch((e) => Promise.reject(e)); + await masp.loadMaspParams("", chainId).catch((e) => Promise.reject(e)); return true; } return masp .fetchAndStoreMaspParams(paramsUrl) - .then(() => masp.loadMaspParams("").then(() => true)) + .then(() => masp.loadMaspParams("", chainId).then(() => true)) .catch((e) => { throw new Error(e); }); @@ -51,7 +52,8 @@ export const SdkProvider: FunctionComponent = ({ const { status: maspParamsStatus } = useQuery({ queryKey: ["sdk"], - queryFn: fetchMaspParams, + enabled: Boolean(parameters.data), + queryFn: async () => await fetchMaspParams(parameters.data!.chainId), retry: 3, retryDelay: 3000, }); diff --git a/apps/namadillo/src/workers/MaspTxWorker.ts b/apps/namadillo/src/workers/MaspTxWorker.ts index e2e280d369..ac2423a87b 100644 --- a/apps/namadillo/src/workers/MaspTxWorker.ts +++ b/apps/namadillo/src/workers/MaspTxWorker.ts @@ -85,7 +85,7 @@ async function shield( await api.apiV1RevealedPublicKeyAddressGet(account.address) ).data.publicKey; - await sdk.masp.loadMaspParams(""); + await sdk.masp.loadMaspParams("", chain.chainId); const encodedTxData = await buildTx( sdk, account, @@ -105,8 +105,8 @@ async function unshield( ): Promise> { const { account, gasConfig, chain, unshieldingProps, vks } = payload; - await sdk.rpc.shieldedSync(vks); - await sdk.masp.loadMaspParams(""); + await sdk.rpc.shieldedSync(vks, chain.chainId); + await sdk.masp.loadMaspParams("", chain.chainId); const encodedTxData = await buildTx( sdk, @@ -127,8 +127,8 @@ async function shieldedTransfer( ): Promise> { const { account, gasConfig, chain, shieldingProps, vks } = payload; - await sdk.rpc.shieldedSync(vks); - await sdk.masp.loadMaspParams(""); + await sdk.rpc.shieldedSync(vks, chain.chainId); + await sdk.masp.loadMaspParams("", chain.chainId); const encodedTxData = await buildTx( sdk, diff --git a/apps/namadillo/src/workers/ShieldedSyncMessages.ts b/apps/namadillo/src/workers/ShieldedSyncMessages.ts index 73cdc9232f..eed30683a0 100644 --- a/apps/namadillo/src/workers/ShieldedSyncMessages.ts +++ b/apps/namadillo/src/workers/ShieldedSyncMessages.ts @@ -11,6 +11,7 @@ export type InitDone = WebWorkerMessage<"init-done", null>; type ShiededSyncPayload = { vks: DatedViewingKey[]; + chainId: string; }; export type Sync = WebWorkerMessage<"sync", ShiededSyncPayload>; export type SyncDone = WebWorkerMessage<"sync-done", null>; diff --git a/apps/namadillo/src/workers/ShieldedSyncWorker.ts b/apps/namadillo/src/workers/ShieldedSyncWorker.ts index 476e481b80..1a367db4d2 100644 --- a/apps/namadillo/src/workers/ShieldedSyncWorker.ts +++ b/apps/namadillo/src/workers/ShieldedSyncWorker.ts @@ -71,7 +71,7 @@ function newSdk( } async function shieldedSync(sdk: Sdk, payload: Sync["payload"]): Promise { - await sdk.rpc.shieldedSync(payload.vks); + await sdk.rpc.shieldedSync(payload.vks, payload.chainId); } Comlink.expose(new Worker()); diff --git a/packages/sdk/src/masp.ts b/packages/sdk/src/masp.ts index 6e68b3db33..4c0685d78b 100644 --- a/packages/sdk/src/masp.ts +++ b/packages/sdk/src/masp.ts @@ -31,11 +31,12 @@ export class Masp { /** * Load stored MASP params * @param pathOrDbName - Path to stored MASP params(nodejs) or name of the database(browser) + * @param chainId - Chain ID to read the MASP params for * @async * @returns void */ - async loadMaspParams(pathOrDbName: string): Promise { - return await this.sdk.load_masp_params(pathOrDbName); + async loadMaspParams(pathOrDbName: string, chainId: string): Promise { + return await this.sdk.load_masp_params(pathOrDbName, chainId); } /** @@ -79,4 +80,13 @@ export class Masp { maspAddress(): string { return this.sdk.masp_address(); } + + /** + * Clear shilded context + * @param chainId - Chain ID to clear the shielded context for + * @returns void + */ + clearShieldedContext(chainId: string): Promise { + return SdkWasm.clear_shielded_context(chainId); + } } diff --git a/packages/sdk/src/rpc/rpc.ts b/packages/sdk/src/rpc/rpc.ts index 5134f64175..3b539b6937 100644 --- a/packages/sdk/src/rpc/rpc.ts +++ b/packages/sdk/src/rpc/rpc.ts @@ -45,10 +45,15 @@ export class Rpc { * @async * @param owner - Owner address * @param tokens - Array of token addresses + * @param chainId - Chain id needed to load specific context * @returns [[tokenAddress, amount]] */ - async queryBalance(owner: string, tokens: string[]): Promise { - return await this.query.query_balance(owner, tokens); + async queryBalance( + owner: string, + tokens: string[], + chainId: string + ): Promise { + return await this.query.query_balance(owner, tokens, chainId); } /** @@ -238,14 +243,14 @@ export class Rpc { * Sync the shielded context * @async * @param vks - Array of dated viewing keys + * @param chainId - Chain ID to sync the shielded context for * @returns */ - async shieldedSync(vks: DatedViewingKey[]): Promise { + async shieldedSync(vks: DatedViewingKey[], chainId: string): Promise { const datedViewingKeys = vks.map( (vk) => new DatedViewingKeyWasm(vk.key, String(vk.birthday)) ); - // TODO: shieled_sync expects an array for strings atm - await this.query.shielded_sync(datedViewingKeys); + await this.query.shielded_sync(datedViewingKeys, chainId); } } diff --git a/packages/shared/lib/src/query.rs b/packages/shared/lib/src/query.rs index 8c51af77f9..dfb29637c0 100644 --- a/packages/shared/lib/src/query.rs +++ b/packages/shared/lib/src/query.rs @@ -324,17 +324,21 @@ impl Query { Ok(result) } - pub async fn shielded_sync(&self, vks: Box<[DatedViewingKey]>) -> Result<(), JsError> { + pub async fn shielded_sync( + &self, + vks: Box<[DatedViewingKey]>, + chain_id: String, + ) -> Result<(), JsError> { let dated_keypairs = vks.iter().map(|vk| vk.0).collect::>(); match &self.masp_client { MaspClient::Indexer(client) => { web_sys::console::info_1(&"Syncing using IndexerMaspClient".into()); - self.sync(client.clone(), dated_keypairs).await? + self.sync(client.clone(), dated_keypairs, chain_id).await? } MaspClient::Ledger(client) => { web_sys::console::info_1(&"Syncing using LedgerMaspClient".into()); - self.sync(client.clone(), dated_keypairs).await? + self.sync(client.clone(), dated_keypairs, chain_id).await? } }; @@ -345,6 +349,7 @@ impl Query { &self, client: C, dated_keypairs: Vec>, + chain_id: String, ) -> Result<(), JsError> where C: NamadaMaspClient + Send + Sync + Unpin + 'static, @@ -374,6 +379,7 @@ impl Query { let env = sync::TaskEnvWeb::new(); let mut shielded_context: ShieldedContext = ShieldedContext::default(); + shielded_context.utils.chain_id = chain_id.clone(); shielded_context .sync(env, config, None, &[], dated_keypairs.as_slice()) @@ -392,11 +398,13 @@ impl Query { &self, xvk: ExtendedViewingKey, tokens: Vec
, + chain_id: String, ) -> Result, JsError> { let viewing_key = ExtendedFullViewingKey::from(xvk).fvk.vk; // We are recreating shielded context to avoid multiple mutable borrows let mut shielded: ShieldedContext = ShieldedContext::default(); + shielded.utils.chain_id = chain_id.clone(); shielded.load().await?; shielded .precompute_asset_types(&self.client, tokens.iter().collect()) @@ -427,6 +435,8 @@ impl Query { &self, owner: String, tokens: Box<[JsValue]>, + // We need to pass chain id to load correct context + chain_id: String, ) -> Result { let tokens: Vec
= tokens .iter() @@ -439,7 +449,7 @@ impl Query { let result = match Address::from_str(&owner) { Ok(addr) => self.query_transparent_balance(addr, tokens).await, Err(e1) => match ExtendedViewingKey::from_str(&owner) { - Ok(xvk) => self.query_shielded_balance(xvk, tokens).await, + Ok(xvk) => self.query_shielded_balance(xvk, tokens, chain_id).await, Err(e2) => return Err(JsError::new(&format!("{} {}", e1, e2))), }, }?; diff --git a/packages/shared/lib/src/sdk/masp/masp_node.rs b/packages/shared/lib/src/sdk/masp/masp_node.rs index 821f72dc62..915fa292a0 100644 --- a/packages/shared/lib/src/sdk/masp/masp_node.rs +++ b/packages/shared/lib/src/sdk/masp/masp_node.rs @@ -32,6 +32,7 @@ const CACHE_FILE_TMP_PREFIX: &str = "shielded_sync.cache.tmp"; pub struct NodeShieldedUtils { #[borsh(skip)] context_dir: PathBuf, + pub chain_id: String, } impl NodeShieldedUtils { @@ -57,7 +58,10 @@ impl NodeShieldedUtils { ContextSyncStatus::Confirmed }; - let utils = Self { context_dir }; + let utils = Self { + context_dir, + chain_id: String::from("node"), + }; ShieldedWallet { utils, @@ -66,6 +70,11 @@ impl NodeShieldedUtils { } } + //TODO: This should not be rexie:Error - it's added only to make the code compile + pub async fn clear(_chain_id: &str) -> Result<(), rexie::Error> { + todo!("Clear not implemented for NodeShieldedUtils"); + } + async fn fetch_params(path: PathBuf, name: &str) { let path = path.to_str().unwrap(); let response = reqwest::get(format!( diff --git a/packages/shared/lib/src/sdk/masp/masp_web.rs b/packages/shared/lib/src/sdk/masp/masp_web.rs index 1c9a060891..cc8082d7f2 100644 --- a/packages/shared/lib/src/sdk/masp/masp_web.rs +++ b/packages/shared/lib/src/sdk/masp/masp_web.rs @@ -9,8 +9,7 @@ use wasm_bindgen::{JsError, JsValue}; use crate::utils::to_bytes; -const DB_PREFIX: &str = "namada_sdk::MASP"; -const SHIELDED_CONTEXT_TABLE: &str = "ShieldedContext"; +const SHIELDED_CONTEXT_PREFIX: &str = "shielded-context"; const SHIELDED_CONTEXT_KEY_CONFIRMED: &str = "shielded-context-confirmed"; const SHIELDED_CONTEXT_KEY_SPECULATIVE: &str = "shielded-context-speculative"; const SHIELDED_CONTEXT_KEY_TEMP: &str = "shielded-context-temp"; @@ -21,6 +20,7 @@ pub struct WebShieldedUtils { spend_param_bytes: Vec, output_param_bytes: Vec, convert_param_bytes: Vec, + pub chain_id: String, } impl WebShieldedUtils { @@ -28,16 +28,21 @@ impl WebShieldedUtils { spend_param_bytes: Vec, output_param_bytes: Vec, convert_param_bytes: Vec, + chain_id: String, ) -> Result, JsError> { let utils = Self { spend_param_bytes, output_param_bytes, convert_param_bytes, + chain_id: chain_id.clone(), }; - let db = Self::build_database().await?; + let db = Self::build_database(&chain_id).await?; - let sync_status = if Self::get_context(&db, false, false).await.is_ok() { + let sync_status = if Self::get_context(&db, false, false, &chain_id) + .await + .is_ok() + { ContextSyncStatus::Speculative } else { ContextSyncStatus::Confirmed @@ -50,14 +55,36 @@ impl WebShieldedUtils { }) } + /// Clear all shielded context data from the database. + pub async fn clear(chain_id: &str) -> Result<(), Error> { + let db = Self::build_database(&chain_id).await?; + let entry = format!("{}-{}", SHIELDED_CONTEXT_PREFIX, chain_id); + + db.transaction(&[entry.clone()], TransactionMode::ReadWrite)? + .store(&entry)? + .delete(&JsValue::from_str(SHIELDED_CONTEXT_KEY_SPECULATIVE)) + .await?; + db.transaction(&[entry.clone()], TransactionMode::ReadWrite)? + .store(&entry)? + .delete(&JsValue::from_str(SHIELDED_CONTEXT_KEY_CONFIRMED)) + .await?; + db.transaction(&[entry.clone()], TransactionMode::ReadWrite)? + .store(&entry)? + .delete(&JsValue::from_str(SHIELDED_CONTEXT_KEY_TEMP)) + .await?; + + Ok(()) + } + fn to_io_err(e: Error) -> std::io::Error { std::io::Error::new(std::io::ErrorKind::Other, e.to_string()) } - pub async fn build_database() -> Result { - let rexie = Rexie::builder(DB_PREFIX) + pub async fn build_database(chain_id: &str) -> Result { + let entry = format!("{}-{}", SHIELDED_CONTEXT_PREFIX, chain_id); + let rexie = Rexie::builder(&entry) .version(1) - .add_object_store(ObjectStore::new(SHIELDED_CONTEXT_TABLE)) + .add_object_store(ObjectStore::new(&entry)) .build() .await?; @@ -69,12 +96,13 @@ impl WebShieldedUtils { context: JsValue, confirmed: bool, cache: bool, + chain_id: &str, ) -> Result<(), Error> { + let entry = format!("{}-{}", SHIELDED_CONTEXT_PREFIX, chain_id); //TODO: add readwriteflush - let transaction = - rexie.transaction(&[SHIELDED_CONTEXT_TABLE], TransactionMode::ReadWrite)?; + let transaction = rexie.transaction(&[entry.clone()], TransactionMode::ReadWrite)?; - let context_store = transaction.store(SHIELDED_CONTEXT_TABLE)?; + let context_store = transaction.store(&entry)?; let key = Self::get_key(confirmed, cache); @@ -85,11 +113,16 @@ impl WebShieldedUtils { Ok(()) } - async fn get_context(rexie: &Rexie, confirmed: bool, cache: bool) -> Result { - let transaction = - rexie.transaction(&[SHIELDED_CONTEXT_TABLE], TransactionMode::ReadOnly)?; + async fn get_context( + rexie: &Rexie, + confirmed: bool, + cache: bool, + chain_id: &str, + ) -> Result { + let entry = format!("{}-{}", SHIELDED_CONTEXT_PREFIX, chain_id); + let transaction = rexie.transaction(&[entry.clone()], TransactionMode::ReadOnly)?; - let context_store = transaction.store(SHIELDED_CONTEXT_TABLE)?; + let context_store = transaction.store(&entry)?; let key = Self::get_key(confirmed, cache); @@ -98,11 +131,11 @@ impl WebShieldedUtils { Ok(context) } - async fn remove_speculative_context(rexie: &Rexie) -> Result<(), Error> { - let transaction = - rexie.transaction(&[SHIELDED_CONTEXT_TABLE], TransactionMode::ReadWrite)?; + async fn remove_speculative_context(rexie: &Rexie, chain_id: &str) -> Result<(), Error> { + let entry = format!("{}-{}", SHIELDED_CONTEXT_PREFIX, chain_id); + let transaction = rexie.transaction(&[entry.clone()], TransactionMode::ReadWrite)?; - let context_store = transaction.store(SHIELDED_CONTEXT_TABLE)?; + let context_store = transaction.store(&entry)?; context_store .delete(&JsValue::from_str(SHIELDED_CONTEXT_KEY_SPECULATIVE)) @@ -144,10 +177,12 @@ impl ShieldedUtils for WebShieldedUtils { ctx: &mut ShieldedWallet, force_confirmed: bool, ) -> std::io::Result<()> { - let db = Self::build_database().await.map_err(Self::to_io_err)?; + let db = Self::build_database(&self.chain_id) + .await + .map_err(Self::to_io_err)?; let confirmed = force_confirmed || get_confirmed(&ctx.sync_status); - let stored_ctx = Self::get_context(&db, confirmed, false) + let stored_ctx = Self::get_context(&db, confirmed, false, &self.chain_id) .await .map_err(Self::to_io_err)?; let stored_ctx_bytes = to_bytes(stored_ctx); @@ -170,15 +205,24 @@ impl ShieldedUtils for WebShieldedUtils { let mut bytes = Vec::new(); ctx.serialize(&mut bytes) .expect("cannot serialize shielded context"); - let db = Self::build_database().await.map_err(Self::to_io_err)?; - let confirmed = get_confirmed(&ctx.sync_status); - Self::set_context(&db, JsValue::from_serde(&bytes).unwrap(), confirmed, false) + let db = Self::build_database(&self.chain_id) .await .map_err(Self::to_io_err)?; + let confirmed = get_confirmed(&ctx.sync_status); + + Self::set_context( + &db, + JsValue::from_serde(&bytes).unwrap(), + confirmed, + false, + &self.chain_id, + ) + .await + .map_err(Self::to_io_err)?; if let ContextSyncStatus::Confirmed = ctx.sync_status { - Self::remove_speculative_context(&db) + Self::remove_speculative_context(&db, &self.chain_id) .await .map_err(Self::to_io_err)?; } @@ -192,18 +236,28 @@ impl ShieldedUtils for WebShieldedUtils { let mut bytes = Vec::new(); cache.serialize(&mut bytes).expect("cannot serialize cache"); - let db = Self::build_database().await.map_err(Self::to_io_err)?; - Self::set_context(&db, JsValue::from_serde(&bytes).unwrap(), false, true) + let db = Self::build_database(&self.chain_id) .await - .map_err(Self::to_io_err) + .map_err(Self::to_io_err)?; + Self::set_context( + &db, + JsValue::from_serde(&bytes).unwrap(), + false, + true, + &self.chain_id, + ) + .await + .map_err(Self::to_io_err) } /// Load a cache of data as part of shielded sync if that /// process gets interrupted. async fn cache_load(&self) -> std::io::Result { - let db = Self::build_database().await.map_err(Self::to_io_err)?; + let db = Self::build_database(&self.chain_id) + .await + .map_err(Self::to_io_err)?; - let stored_cache = Self::get_context(&db, false, true) + let stored_cache = Self::get_context(&db, false, true, &self.chain_id) .await .map_err(Self::to_io_err)?; let stored_cache_bytes = to_bytes(stored_cache); diff --git a/packages/shared/lib/src/sdk/mod.rs b/packages/shared/lib/src/sdk/mod.rs index 69493b1ed5..81c44c54de 100644 --- a/packages/shared/lib/src/sdk/mod.rs +++ b/packages/shared/lib/src/sdk/mod.rs @@ -99,6 +99,12 @@ impl Sdk { } } + pub async fn clear_shielded_context(chain_id: String) -> Result<(), JsValue> { + masp::JSShieldedUtils::clear(&chain_id).await?; + + Ok(()) + } + pub async fn has_masp_params() -> Result { let has = has_masp_params().await?; @@ -111,7 +117,11 @@ impl Sdk { } #[cfg(feature = "web")] - pub async fn load_masp_params(&self, _db_name: JsValue) -> Result<(), JsValue> { + pub async fn load_masp_params( + &self, + _db_name: JsValue, + chain_id: String, + ) -> Result<(), JsValue> { // _dn_name is not used in the web version for a time being let params = get_masp_params().await?; let params_iter = js_sys::try_iter(¶ms)?.ok_or("Can't iterate over JsValue")?; @@ -125,7 +135,9 @@ impl Sdk { assert_eq!(params_bytes.next(), None); let mut shielded = self.namada.shielded_mut().await; - *shielded = ShieldedContext::new(masp::JSShieldedUtils::new(spend, output, convert).await?); + *shielded = ShieldedContext::new( + masp::JSShieldedUtils::new(spend, output, convert, chain_id).await?, + ); Ok(()) } diff --git a/packages/shared/package.json b/packages/shared/package.json index cf648fda01..97ae090758 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -22,7 +22,7 @@ "test-wasm:ci": "yarn wasm:ts:node && cd ./lib && wasm-pack test --node -- --features nodejs", "wasm:build:node:multicore": "yarn wasm:ts:node && node ./scripts/build.js --target nodejs --release --multicore", "wasm:build:node:dev": "yarn wasm:ts:node && node ./scripts/build.js --target nodejs", - "wasm:build:node:dev:multicore": "yarn wasm:ts:node && node ./scripts/build.js --target node --multicore" + "wasm:build:node:dev:multicore": "yarn wasm:ts:node && node ./scripts/build.js --target nodejs --multicore" }, "dependencies": { "@dao-xyz/borsh": "^5.1.5",