diff --git a/apps/extension/package.json b/apps/extension/package.json index 49b47b6e6..f127d1d27 100644 --- a/apps/extension/package.json +++ b/apps/extension/package.json @@ -1,6 +1,6 @@ { "name": "@namada/extension", - "version": "0.1.0", + "version": "0.2.0", "description": "Namada Browser Extension", "repository": "https://github.com/anoma/namada-interface/", "author": "Heliax Dev ", diff --git a/apps/extension/src/App/AppContent.tsx b/apps/extension/src/App/AppContent.tsx index 27c1d0945..702e11329 100644 --- a/apps/extension/src/App/AppContent.tsx +++ b/apps/extension/src/App/AppContent.tsx @@ -19,8 +19,7 @@ import { ViewMnemonic, } from "./Accounts"; import { ParentAccounts } from "./Accounts/ParentAccounts"; -import { ConnectedSites } from "./ConnectedSites"; -import { ChangePassword } from "./Settings/ChangePassword"; +import { ChangePassword, ConnectedSites, Network } from "./Settings"; import { Setup } from "./Setup"; import routes from "./routes"; import { LoadingStatus } from "./types"; @@ -134,6 +133,7 @@ export const AppContent = (): JSX.Element => { /> } /> + } /> {/* Routes that depend on a parent account existing in storage */} {accounts.length > 0 && ( <> diff --git a/apps/extension/src/App/Common/AppHeaderNavigation.tsx b/apps/extension/src/App/Common/AppHeaderNavigation.tsx index 238384d57..56a9356e4 100644 --- a/apps/extension/src/App/Common/AppHeaderNavigation.tsx +++ b/apps/extension/src/App/Common/AppHeaderNavigation.tsx @@ -72,6 +72,12 @@ export const AppHeaderNavigation = ({ > Connected Sites +
  • goTo(routes.network)} + > + Network +
  • Lock Wallet
  • diff --git a/apps/extension/src/App/ConnectedSites.tsx b/apps/extension/src/App/Settings/ConnectedSites.tsx similarity index 100% rename from apps/extension/src/App/ConnectedSites.tsx rename to apps/extension/src/App/Settings/ConnectedSites.tsx diff --git a/apps/extension/src/App/Settings/Network.tsx b/apps/extension/src/App/Settings/Network.tsx new file mode 100644 index 000000000..a79e8bf67 --- /dev/null +++ b/apps/extension/src/App/Settings/Network.tsx @@ -0,0 +1,131 @@ +import { sanitize } from "dompurify"; + +import { + ActionButton, + Alert, + GapPatterns, + Heading, + Input, + Stack, +} from "@namada/components"; +import { isUrlValid } from "@namada/utils"; +import { UpdateChainMsg } from "background/chains"; +import { useRequester } from "hooks/useRequester"; +import { GetChainMsg } from "provider"; +import React, { useCallback, useEffect, useState } from "react"; +import { Ports } from "router"; + +enum Status { + Unsubmitted, + Pending, + Complete, + Failed, +} + +export const Network = (): JSX.Element => { + const requester = useRequester(); + const [chainId, setChainId] = useState(""); + const [url, setUrl] = useState(""); + const [status, setStatus] = useState(Status.Unsubmitted); + const [errorMessage, setErrorMessage] = useState(""); + + // TODO: Validate URL and sanitize inputs! + const shouldDisableSubmit = status === Status.Pending || !chainId || !url; + + useEffect(() => { + (async () => { + try { + const chain = await requester.sendMessage( + Ports.Background, + new GetChainMsg() + ); + if (!chain) { + throw new Error("Chain not found!"); + } + const { chainId, rpc } = chain; + setUrl(rpc); + setChainId(chainId); + } catch (e) { + setErrorMessage(`${e}`); + } + })(); + }, []); + + const handleSubmit = useCallback( + async (e: React.FormEvent): Promise => { + e.preventDefault(); + setStatus(Status.Pending); + setErrorMessage(""); + const sanitizedChainId = sanitize(chainId); + const sanitizedUrl = sanitize(url); + + // Validate sanitized chain ID + if (!sanitizedChainId) { + setErrorMessage("Invalid chain ID!"); + setStatus(Status.Failed); + return; + } + + // Validate sanitized URL + const isValidUrl = isUrlValid(sanitizedUrl); + if (!isValidUrl) { + setErrorMessage("Invalid URL!"); + setStatus(Status.Failed); + return; + } + + try { + await requester.sendMessage( + Ports.Background, + new UpdateChainMsg(sanitizedChainId, sanitizedUrl) + ); + setStatus(Status.Complete); + } catch (err) { + setStatus(Status.Failed); + setErrorMessage(`${err}`); + } + }, + [chainId, url] + ); + + return ( + + + Network + + + setChainId(e.target.value)} + error={chainId.length === 0 ? "URL is required!" : ""} + /> + setUrl(e.target.value)} + error={url.length === 0 ? "URL is required!" : ""} + /> + {errorMessage && {errorMessage}} + {status === Status.Complete && ( + Successfully updated network! + )} + + + Submit + + + ); +}; diff --git a/apps/extension/src/App/Settings/index.ts b/apps/extension/src/App/Settings/index.ts new file mode 100644 index 000000000..5073e9d3a --- /dev/null +++ b/apps/extension/src/App/Settings/index.ts @@ -0,0 +1,3 @@ +export * from "./ChangePassword"; +export * from "./ConnectedSites"; +export * from "./Network"; diff --git a/apps/extension/src/App/routes.ts b/apps/extension/src/App/routes.ts index 4ef3e3c68..fede421b9 100644 --- a/apps/extension/src/App/routes.ts +++ b/apps/extension/src/App/routes.ts @@ -5,7 +5,7 @@ export default { setup: (): string => `/setup`, changePassword: (): string => `/change-password`, connectedSites: (): string => `/connected-sites`, - + network: (): string => `/network`, viewAccountList: () => `/accounts/view`, viewAccountMnemonic: (accountId: string = ":accountId") => `/accounts/mnemonic/${accountId}`, diff --git a/apps/extension/src/background/chains/handler.ts b/apps/extension/src/background/chains/handler.ts index cddb6a167..40d84ae17 100644 --- a/apps/extension/src/background/chains/handler.ts +++ b/apps/extension/src/background/chains/handler.ts @@ -1,62 +1,33 @@ -import { Chain } from "@namada/types"; -import { Handler, Env, Message, InternalHandler } from "router"; +import { GetChainMsg } from "provider/messages"; +import { Env, Handler, InternalHandler, Message } from "router"; +import { UpdateChainMsg } from "./messages"; import { ChainsService } from "./service"; -import { RemoveChainMsg } from "./messages"; -import { SuggestChainMsg, GetChainsMsg, GetChainMsg } from "provider/messages"; - -type Writeable = { -readonly [P in keyof T]: T[P] }; export const getHandler: (service: ChainsService) => Handler = (service) => { return (env: Env, msg: Message) => { switch (msg.constructor) { case GetChainMsg: return handleGetChainMsg(service)(env, msg as GetChainMsg); - case GetChainsMsg: - return handleGetChainsMsg(service)(env, msg as GetChainsMsg); - case SuggestChainMsg: - return handleSuggestChainMsg(service)(env, msg as SuggestChainMsg); - case RemoveChainMsg: - return handleRemoveChainMsg(service)(env, msg as RemoveChainMsg); + case UpdateChainMsg: + return handleUpdateChainMsg(service)(env, msg as UpdateChainMsg); default: throw new Error("Unknown msg type"); } }; }; -const handleGetChainsMsg: ( - service: ChainsService -) => InternalHandler = (service) => { - return async () => { - return await service.getChains(); - }; -}; - const handleGetChainMsg: ( service: ChainsService ) => InternalHandler = (service) => { - return async (_, msg) => { - return await service.getChain(msg.chainId); - }; -}; - -const handleSuggestChainMsg: ( - service: ChainsService -) => InternalHandler = (service) => { - return async (env, msg) => { - if (await service.hasChain(msg.chain.chainId)) { - return; - } - - const chain = msg.chain as Writeable; - await service.suggestChain(env, chain, msg.origin); + return async () => { + return await service.getChain(); }; }; -const handleRemoveChainMsg: ( +const handleUpdateChainMsg: ( service: ChainsService -) => InternalHandler = (service) => { - return async (_, msg) => { - await service.removeChain(msg.chainId); - return await service.getChains(); +) => InternalHandler = (service) => { + return async (_, { chainId, url }) => { + return await service.updateChain(chainId, url); }; }; diff --git a/apps/extension/src/background/chains/index.ts b/apps/extension/src/background/chains/index.ts index 31c6a0d52..a03214a55 100644 --- a/apps/extension/src/background/chains/index.ts +++ b/apps/extension/src/background/chains/index.ts @@ -1,4 +1,4 @@ -export * from "./messages"; export * from "./handler"; -export * from "./service"; export * from "./init"; +export * from "./messages"; +export * from "./service"; diff --git a/apps/extension/src/background/chains/init.ts b/apps/extension/src/background/chains/init.ts index 3e5ad91df..4a1625c70 100644 --- a/apps/extension/src/background/chains/init.ts +++ b/apps/extension/src/background/chains/init.ts @@ -1,15 +1,13 @@ +import { GetChainMsg } from "provider/messages"; import { Router } from "router"; import { ROUTE } from "./constants"; -import { RemoveChainMsg } from "./messages"; -import { SuggestChainMsg, GetChainsMsg, GetChainMsg } from "provider/messages"; import { getHandler } from "./handler"; +import { UpdateChainMsg } from "./messages"; import { ChainsService } from "./service"; export function init(router: Router, service: ChainsService): void { router.registerMessage(GetChainMsg); - router.registerMessage(GetChainsMsg); - router.registerMessage(SuggestChainMsg); - router.registerMessage(RemoveChainMsg); + router.registerMessage(UpdateChainMsg); router.addHandler(ROUTE, getHandler(service)); } diff --git a/apps/extension/src/background/chains/messages.ts b/apps/extension/src/background/chains/messages.ts index 341712b83..265e1ccc5 100644 --- a/apps/extension/src/background/chains/messages.ts +++ b/apps/extension/src/background/chains/messages.ts @@ -1,18 +1,19 @@ -import { Chain } from "@namada/types"; import { Message } from "router"; import { ROUTE } from "./constants"; enum MessageType { - RemoveChain = "remove-chain", - Connect = "connect", + UpdateChain = "update-chain", } -export class RemoveChainMsg extends Message { +export class UpdateChainMsg extends Message { public static type(): MessageType { - return MessageType.RemoveChain; + return MessageType.UpdateChain; } - constructor(public readonly chainId: string) { + constructor( + public readonly chainId: string, + public readonly url: string + ) { super(); } @@ -20,6 +21,9 @@ export class RemoveChainMsg extends Message { if (!this.chainId) { throw new Error("Chain ID not provided!"); } + if (!this.url) { + throw new Error("URL not provided!"); + } } route(): string { @@ -27,6 +31,6 @@ export class RemoveChainMsg extends Message { } type(): string { - return RemoveChainMsg.type(); + return UpdateChainMsg.type(); } } diff --git a/apps/extension/src/background/chains/service.ts b/apps/extension/src/background/chains/service.ts index 4caa733c0..36926136b 100644 --- a/apps/extension/src/background/chains/service.ts +++ b/apps/extension/src/background/chains/service.ts @@ -1,94 +1,37 @@ +import { chains, defaultChainId } from "@namada/chains"; import { KVStore } from "@namada/storage"; -import { debounce } from "@namada/utils"; import { Chain } from "@namada/types"; -import { Env, KVKeys } from "router"; +import { ExtensionBroadcaster } from "extension"; -type ChainRemovedHandler = (chainId: string, identifier: string) => void; +export const CHAINS_KEY = "chains"; export class ChainsService { - protected onChainRemovedHandlers: ChainRemovedHandler[] = []; - constructor( - protected readonly kvStore: KVStore, - protected readonly defaultChains: Chain[] + protected readonly chainsStore: KVStore, + protected readonly broadcaster: ExtensionBroadcaster ) {} - readonly getChains: () => Promise = debounce(async () => { - const chains = [...this.defaultChains]; - const suggestedChains: Chain[] = await this.getSuggestedChains(); - return chains.concat(suggestedChains); - }); - - async getChain(chainId: string): Promise { - const chain = (await this.getChains()).find((chain) => { - return chain.chainId === chainId; - }); - + async getChain(): Promise { + const chain = await this.chainsStore.get(CHAINS_KEY); if (!chain) { - throw new Error(`There is no chain info for ${chainId}`); + // Initialize default chain in storage + const defaultChain = chains[defaultChainId]; + await this.chainsStore.set(CHAINS_KEY, defaultChain); + return defaultChain; } return chain; } - async getChainCoinType(chainId: string): Promise { - const chain = await this.getChain(chainId); - + async updateChain(chainId: string, url: string): Promise { + const chain = await this.getChain(); if (!chain) { - throw new Error(`There is no chain info for ${chainId}`); - } - - return chain.bip44.coinType; - } - - async hasChain(chainId: string): Promise { - return ( - (await this.getChains()).find((chain) => { - return chain.chainId === chainId; - }) != null - ); - } - - async suggestChain(env: Env, chain: Chain, origin: string): Promise { - // TODO: Validate against schema - // chain = await ChainSchema.validateAsync(chain, { - // stripUnknown: true, - // }); - console.info("suggestChain", { env, chain, origin }); - await this.addChain(chain); - } - - async getSuggestedChains(): Promise { - return (await this.kvStore.get(KVKeys.Chains)) ?? []; - } - - async addChain(chain: Chain): Promise { - const { chainId } = chain; - if (await this.hasChain(chainId)) { - throw new Error(`Chain with ID ${chainId} is already registered`); + throw new Error("No chain found!"); } - - const savedChains = (await this.kvStore.get(KVKeys.Chains)) ?? []; - - savedChains.push(chain); - - await this.kvStore.set(KVKeys.Chains, savedChains); - } - - async removeChain(chainId: string): Promise { - if (!(await this.hasChain(chainId))) { - throw new Error("Chain is not registered"); - } - - const chains = (await this.kvStore.get(KVKeys.Chains)) ?? []; - - const filteredChains = chains.filter((chain: Chain) => { - return chain.chainId !== chainId; + await this.chainsStore.set(CHAINS_KEY, { + ...chain, + chainId, + rpc: url, }); - - await this.kvStore.set(KVKeys.Chains, filteredChains); - } - - addChainRemovedHandler(handler: ChainRemovedHandler): void { - this.onChainRemovedHandlers.push(handler); + this.broadcaster.updateNetwork(); } } diff --git a/apps/extension/src/background/index.ts b/apps/extension/src/background/index.ts index 46dd92593..b204fa73d 100644 --- a/apps/extension/src/background/index.ts +++ b/apps/extension/src/background/index.ts @@ -1,6 +1,4 @@ -import { ProxyMappings } from "@namada/chains"; import { init as initCrypto } from "@namada/crypto/src/init"; -import { Query, Sdk } from "@namada/shared"; import { init as initShared } from "@namada/shared/src/init"; import { ExtensionKVStore, @@ -21,8 +19,10 @@ import { } from "extension"; import { KVPrefix, Ports } from "router"; import { ApprovalsService, init as initApprovals } from "./approvals"; +import { ChainsService, init as initChains } from "./chains"; import { KeyRingService, UtilityStore, init as initKeyRing } from "./keyring"; import { LedgerService, init as initLedger } from "./ledger"; +import { SdkService } from "./sdk/service"; import { VaultService, init as initVault } from "./vault"; const store = new IndexedDBKVStore(KVPrefix.IndexedDB); @@ -41,21 +41,16 @@ const approvedOriginsStore = new ExtensionKVStore(KVPrefix.LocalStorage, { get: browser.storage.local.get, set: browser.storage.local.set, }); +const chainStore = new ExtensionKVStore(KVPrefix.LocalStorage, { + get: browser.storage.local.get, + set: browser.storage.local.set, +}); const revealedPKStore = new ExtensionKVStore(KVPrefix.RevealedPK, { get: browser.storage.local.get, set: browser.storage.local.set, }); const txStore = new MemoryKVStore(KVPrefix.Memory); -const DEFAULT_URL = - "https://d3brk13lbhxfdb.cloudfront.net/qc-testnet-5.1.025a61165acd05e"; -const { NAMADA_INTERFACE_PROXY, NAMADA_INTERFACE_NAMADA_URL = DEFAULT_URL } = - process.env; - -const NamadaRpcEndpoint = NAMADA_INTERFACE_PROXY - ? ProxyMappings["namada"] - : NAMADA_INTERFACE_NAMADA_URL; - const messenger = new ExtensionMessenger(); const router = new ExtensionRouter( ContentScriptEnv.produceEnv, @@ -76,8 +71,6 @@ const init = new Promise(async (resolve) => { wasm.arrayBuffer() ); await initShared(sharedWasm); - const sdk = new Sdk(NamadaRpcEndpoint); - const query = new Query(NamadaRpcEndpoint); const routerId = await getNamadaRouterId(extensionStore); const requester = new ExtensionRequester(messenger, routerId); @@ -89,24 +82,25 @@ const init = new Promise(async (resolve) => { cryptoMemory, broadcaster ); + const sdkService = new SdkService(chainStore); + const chainsService = new ChainsService(chainStore, broadcaster); const keyRingService = new KeyRingService( vaultService, + sdkService, utilityStore, connectedTabsStore, extensionStore, - sdk, - query, cryptoMemory, requester, broadcaster ); const ledgerService = new LedgerService( keyRingService, + sdkService, store, connectedTabsStore, txStore, revealedPKStore, - sdk, requester, broadcaster ); @@ -121,10 +115,10 @@ const init = new Promise(async (resolve) => { // Initialize messages and handlers initApprovals(router, approvalsService); + initChains(router, chainsService); initKeyRing(router, keyRingService); initLedger(router, ledgerService); initVault(router, vaultService); - resolve(); }); diff --git a/apps/extension/src/background/keyring/keyring.ts b/apps/extension/src/background/keyring/keyring.ts index 3510d5626..f8d4b5c8e 100644 --- a/apps/extension/src/background/keyring/keyring.ts +++ b/apps/extension/src/background/keyring/keyring.ts @@ -16,8 +16,6 @@ import { ExtendedSpendingKey, ExtendedViewingKey, PaymentAddress, - Query, - Sdk, } from "@namada/shared"; import { KVStore } from "@namada/storage"; import { @@ -33,6 +31,12 @@ import { Tokens, TransferMsgValue, } from "@namada/types"; +import { + Result, + assertNever, + makeBip44PathArray, + truncateInMiddle, +} from "@namada/utils"; import { AccountSecret, @@ -46,13 +50,7 @@ import { UtilityStore, } from "./types"; -import { - Result, - assertNever, - makeBip44PathArray, - truncateInMiddle, -} from "@namada/utils"; - +import { SdkService } from "background/sdk"; import { VaultService } from "background/vault"; import { generateId } from "utils"; @@ -78,12 +76,11 @@ export class KeyRing { constructor( protected readonly vaultService: VaultService, + protected readonly sdkService: SdkService, protected readonly utilityStore: KVStore, protected readonly extensionStore: KVStore, - protected readonly sdk: Sdk, - protected readonly query: Query, protected readonly cryptoMemory: WebAssembly.Memory - ) {} + ) { } public get status(): KeyRingStatus { return this._status; @@ -652,12 +649,12 @@ export class KeyRing { try { const { source } = deserialize(Buffer.from(bondMsg), SubmitBondMsgValue); const signingKey = await this.getSigningKey(source); + const sdk = await this.sdkService.getSdk(); + await sdk.reveal_pk(signingKey, txMsg); - await this.sdk.reveal_pk(signingKey, txMsg); - - const builtTx = await this.sdk.build_bond(bondMsg, txMsg); - const txBytes = await this.sdk.sign_tx(builtTx, txMsg, signingKey); - await this.sdk.process_tx(txBytes, txMsg); + const builtTx = await sdk.build_bond(bondMsg, txMsg); + const txBytes = await sdk.sign_tx(builtTx, txMsg, signingKey); + await sdk.process_tx(txBytes, txMsg); } catch (e) { throw new Error(`Could not submit bond tx: ${e}`); } @@ -673,6 +670,7 @@ export class KeyRing { async submitUnbond(unbondMsg: Uint8Array, txMsg: Uint8Array): Promise { await this.vaultService.assertIsUnlocked(); + const sdk = await this.sdkService.getSdk(); try { const { source } = deserialize( Buffer.from(unbondMsg), @@ -680,11 +678,11 @@ export class KeyRing { ); const signingKey = await this.getSigningKey(source); - await this.sdk.reveal_pk(signingKey, txMsg); + await sdk.reveal_pk(signingKey, txMsg); - const builtTx = await this.sdk.build_unbond(unbondMsg, txMsg); - const txBytes = await this.sdk.sign_tx(builtTx, txMsg, signingKey); - await this.sdk.process_tx(txBytes, txMsg); + const builtTx = await sdk.build_unbond(unbondMsg, txMsg); + const txBytes = await sdk.sign_tx(builtTx, txMsg, signingKey); + await sdk.process_tx(txBytes, txMsg); } catch (e) { throw new Error(`Could not submit unbond tx: ${e}`); } @@ -695,6 +693,7 @@ export class KeyRing { txMsg: Uint8Array ): Promise { await this.vaultService.assertIsUnlocked(); + const sdk = await this.sdkService.getSdk(); try { const { source } = deserialize( Buffer.from(withdrawMsg), @@ -702,11 +701,11 @@ export class KeyRing { ); const signingKey = await this.getSigningKey(source); - await this.sdk.reveal_pk(signingKey, txMsg); + await sdk.reveal_pk(signingKey, txMsg); - const builtTx = await this.sdk.build_withdraw(withdrawMsg, txMsg); - const txBytes = await this.sdk.sign_tx(builtTx, txMsg, signingKey); - await this.sdk.process_tx(txBytes, txMsg); + const builtTx = await sdk.build_withdraw(withdrawMsg, txMsg); + const txBytes = await sdk.sign_tx(builtTx, txMsg, signingKey); + await sdk.process_tx(txBytes, txMsg); } catch (e) { throw new Error(`Could not submit withdraw tx: ${e}`); } @@ -717,6 +716,7 @@ export class KeyRing { txMsg: Uint8Array ): Promise { await this.vaultService.assertIsUnlocked(); + const sdk = await this.sdkService.getSdk(); try { const { signer } = deserialize( Buffer.from(voteProposalMsg), @@ -724,15 +724,12 @@ export class KeyRing { ); const signingKey = await this.getSigningKey(signer); - await this.sdk.reveal_pk(signingKey, txMsg); + await sdk.reveal_pk(signingKey, txMsg); - const builtTx = await this.sdk.build_vote_proposal( - voteProposalMsg, - txMsg - ); + const builtTx = await sdk.build_vote_proposal(voteProposalMsg, txMsg); - const txBytes = await this.sdk.sign_tx(builtTx, txMsg, signingKey); - await this.sdk.process_tx(txBytes, txMsg); + const txBytes = await sdk.sign_tx(builtTx, txMsg, signingKey); + await sdk.process_tx(txBytes, txMsg); } catch (e) { throw new Error(`Could not submit vote proposal tx: ${e}`); } @@ -775,6 +772,7 @@ export class KeyRing { txMsg: Uint8Array ): Promise { await this.vaultService.assertIsUnlocked(); + const sdk = await this.sdkService.getSdk(); try { const { source } = deserialize( Buffer.from(ibcTransferMsg), @@ -782,11 +780,11 @@ export class KeyRing { ); const signingKey = await this.getSigningKey(source); - await this.sdk.reveal_pk(signingKey, txMsg); + await sdk.reveal_pk(signingKey, txMsg); - const builtTx = await this.sdk.build_ibc_transfer(ibcTransferMsg, txMsg); - const txBytes = await this.sdk.sign_tx(builtTx, txMsg, signingKey); - await this.sdk.process_tx(txBytes, txMsg); + const builtTx = await sdk.build_ibc_transfer(ibcTransferMsg, txMsg); + const txBytes = await sdk.sign_tx(builtTx, txMsg, signingKey); + await sdk.process_tx(txBytes, txMsg); } catch (e) { throw new Error(`Could not submit ibc transfer tx: ${e}`); } @@ -797,6 +795,7 @@ export class KeyRing { txMsg: Uint8Array ): Promise { await this.vaultService.assertIsUnlocked(); + const sdk = await this.sdkService.getSdk(); try { const { sender } = deserialize( Buffer.from(ethBridgeTransferMsg), @@ -804,14 +803,14 @@ export class KeyRing { ); const signingKey = await this.getSigningKey(sender); - await this.sdk.reveal_pk(signingKey, txMsg); + await sdk.reveal_pk(signingKey, txMsg); - const builtTx = await this.sdk.build_eth_bridge_transfer( + const builtTx = await sdk.build_eth_bridge_transfer( ethBridgeTransferMsg, txMsg ); - const txBytes = await this.sdk.sign_tx(builtTx, txMsg, signingKey); - await this.sdk.process_tx(txBytes, txMsg); + const txBytes = await sdk.sign_tx(builtTx, txMsg, signingKey); + await sdk.process_tx(txBytes, txMsg); } catch (e) { throw new Error(`Could not submit submit_eth_bridge_transfer tx: ${e}`); } @@ -857,12 +856,13 @@ export class KeyRing { async queryBalances( owner: string ): Promise<{ token: string; amount: string }[]> { + const query = await this.sdkService.getQuery(); const tokenAddresses: string[] = Object.values(Tokens) .filter((token) => token.address) .map(({ address }) => address); try { - return (await this.query.query_balance(owner, tokenAddresses)).map( + return (await query.query_balance(owner, tokenAddresses)).map( ([token, amount]: [string, string]) => { return { token, @@ -875,4 +875,9 @@ export class KeyRing { return []; } } + + async queryPublicKey(address: string): Promise { + const query = await this.sdkService.getQuery(); + return await query.query_public_key(address); + } } diff --git a/apps/extension/src/background/keyring/service.ts b/apps/extension/src/background/keyring/service.ts index 05eec5818..dc4045908 100644 --- a/apps/extension/src/background/keyring/service.ts +++ b/apps/extension/src/background/keyring/service.ts @@ -1,7 +1,7 @@ import { fromBase64, fromHex } from "@cosmjs/encoding"; import { PhraseSize } from "@namada/crypto"; -import { public_key_to_bech32, Query, Sdk, TxType } from "@namada/shared"; +import { public_key_to_bech32, Sdk, TxType } from "@namada/shared"; import { IndexedDBKVStore, KVStore } from "@namada/storage"; import { AccountType, Bip44Path, DerivedAccount } from "@namada/types"; import { Result, truncateInMiddle } from "@namada/utils"; @@ -12,6 +12,7 @@ import { OFFSCREEN_TARGET, SUBMIT_TRANSFER_MSG_TYPE, } from "background/offscreen"; +import { SdkService } from "background/sdk/service"; import { VaultService } from "background/vault"; import { init as initSubmitTransferWebWorker } from "background/web-workers"; import { @@ -37,23 +38,20 @@ export class KeyRingService { private _keyRing: KeyRing; constructor( - //protected readonly kvStore: KVStore, protected readonly vaultService: VaultService, + protected readonly sdkService: SdkService, protected readonly utilityStore: KVStore, protected readonly connectedTabsStore: KVStore, protected readonly extensionStore: KVStore, - protected readonly sdk: Sdk, - protected readonly query: Query, protected readonly cryptoMemory: WebAssembly.Memory, protected readonly requester: ExtensionRequester, protected readonly broadcaster: ExtensionBroadcaster ) { this._keyRing = new KeyRing( vaultService, + sdkService, utilityStore, extensionStore, - sdk, - query, cryptoMemory ); } @@ -443,7 +441,7 @@ export class KeyRingService { } async queryPublicKey(address: string): Promise { - return await this.query.query_public_key(address); + return await this._keyRing.queryPublicKey(address); } async checkDurability(): Promise { diff --git a/apps/extension/src/background/ledger/service.ts b/apps/extension/src/background/ledger/service.ts index 25a4d8ba7..938931578 100644 --- a/apps/extension/src/background/ledger/service.ts +++ b/apps/extension/src/background/ledger/service.ts @@ -2,13 +2,14 @@ import { fromBase64 } from "@cosmjs/encoding"; import { deserialize } from "@dao-xyz/borsh"; import { chains, defaultChainId } from "@namada/chains"; -import { ResponseSign } from "@zondax/ledger-namada"; -import { Sdk, TxType } from "@namada/shared"; +import { TxType } from "@namada/shared"; import { KVStore } from "@namada/storage"; import { AccountType, TxMsgValue } from "@namada/types"; import { makeBip44Path } from "@namada/utils"; +import { ResponseSign } from "@zondax/ledger-namada"; import { TxStore } from "background/approvals"; import { AccountStore, KeyRingService, TabStore } from "background/keyring"; +import { SdkService } from "background/sdk"; import { ExtensionBroadcaster, ExtensionRequester } from "extension"; import { encodeSignature } from "utils"; @@ -18,14 +19,14 @@ const REVEALED_PK_STORE = "revealed-pk-store"; export class LedgerService { constructor( protected readonly keyringService: KeyRingService, + protected readonly sdkService: SdkService, protected readonly kvStore: KVStore, protected readonly connectedTabsStore: KVStore, protected readonly txStore: KVStore, protected readonly revealedPKStore: KVStore, - protected readonly sdk: Sdk, protected readonly requester: ExtensionRequester, protected readonly broadcaster: ExtensionBroadcaster - ) {} + ) { } async getRevealPKBytes( txMsg: string @@ -44,9 +45,8 @@ export class LedgerService { } // Query account from Ledger storage to determine path for signer - const account = await this.keyringService.findParentByPublicKey( - publicKey - ); + const account = + await this.keyringService.findParentByPublicKey(publicKey); if (!account) { throw new Error(`Ledger account not found for ${publicKey}`); @@ -56,7 +56,8 @@ export class LedgerService { throw new Error(`Returned Account is not a Ledger`); } - const bytes = await this.sdk.build_tx( + const sdk = await this.sdkService.getSdk(); + const bytes = await sdk.build_tx( TxType.RevealPK, new Uint8Array(), // TODO: this is a dummy value. Is there a cleaner way? fromBase64(txMsg), @@ -86,14 +87,9 @@ export class LedgerService { // Serialize signatures const sig = encodeSignature(signature); - const signedTxBytes = await this.sdk.append_signature( - fromBase64(bytes), - sig - ); - await this.sdk.process_tx( - signedTxBytes, - fromBase64(txMsg), - ); + const sdk = await this.sdkService.getSdk(); + const signedTxBytes = await sdk.append_signature(fromBase64(bytes), sig); + await sdk.process_tx(signedTxBytes, fromBase64(txMsg)); } catch (e) { console.warn(e); } @@ -123,16 +119,10 @@ export class LedgerService { const sig = encodeSignature(signature); await this.broadcaster.startTx(msgId, txType); - + const sdk = await this.sdkService.getSdk(); try { - const signedTxBytes = await this.sdk.append_signature( - fromBase64(bytes), - sig - ); - await this.sdk.process_tx( - signedTxBytes, - fromBase64(txMsg), - ); + const signedTxBytes = await sdk.append_signature(fromBase64(bytes), sig); + await sdk.process_tx(signedTxBytes, fromBase64(txMsg)); // Clear pending tx if successful await this.txStore.set(msgId, null); @@ -182,7 +172,8 @@ export class LedgerService { throw new Error(`Ledger account not found for ${address}`); } - const bytes = await this.sdk.build_tx( + const sdk = await this.sdkService.getSdk(); + const bytes = await sdk.build_tx( txType, fromBase64(specificMsg), fromBase64(txMsg), diff --git a/apps/extension/src/background/sdk/index.ts b/apps/extension/src/background/sdk/index.ts new file mode 100644 index 000000000..6261f8963 --- /dev/null +++ b/apps/extension/src/background/sdk/index.ts @@ -0,0 +1 @@ +export * from "./service"; diff --git a/apps/extension/src/background/sdk/service.ts b/apps/extension/src/background/sdk/service.ts new file mode 100644 index 000000000..78d47e68f --- /dev/null +++ b/apps/extension/src/background/sdk/service.ts @@ -0,0 +1,29 @@ +import { Query, Sdk } from "@namada/shared"; +import { KVStore } from "@namada/storage"; +import { Chain } from "@namada/types"; +import { CHAINS_KEY } from "background/chains"; + +export class SdkService { + constructor(protected readonly chainStore: KVStore) { } + + private async _getRpc(): Promise { + // Pull chain config from store, as the RPC value may have changed: + const chain = await this.chainStore.get(CHAINS_KEY); + + if (!chain) { + throw new Error("No chain found!"); + } + const { rpc } = chain; + return rpc; + } + + async getSdk(): Promise { + const rpc = await this._getRpc(); + return new Sdk(rpc); + } + + async getQuery(): Promise { + const rpc = await this._getRpc(); + return new Query(rpc); + } +} diff --git a/apps/extension/src/content/events.ts b/apps/extension/src/content/events.ts index 9342af667..abd1e12cb 100644 --- a/apps/extension/src/content/events.ts +++ b/apps/extension/src/content/events.ts @@ -1,7 +1,7 @@ import { Events } from "@namada/types"; -import { Message, Router, Routes } from "../router"; import { TxType } from "@namada/shared"; +import { Message, Router, Routes } from "../router"; // Used by Firefox to copy the object from the content script scope to the // page script scope. @@ -51,6 +51,28 @@ export class UpdatedBalancesEventMsg extends Message { } } +export class NetworkChangedEventMsg extends Message { + public static type(): Events { + return Events.NetworkChanged; + } + + constructor() { + super(); + } + + validate(): void { + return; + } + + route(): string { + return Routes.InteractionForeground; + } + + type(): string { + return NetworkChangedEventMsg.type(); + } +} + export class UpdatedStakingEventMsg extends Message { public static type(): Events { return Events.UpdatedStaking; @@ -78,7 +100,10 @@ export class TxStartedEvent extends Message { return Events.TxStarted; } - constructor(public readonly msgId: string, public readonly txType: TxType) { + constructor( + public readonly msgId: string, + public readonly txType: TxType + ) { super(); } @@ -165,7 +190,7 @@ export class VaultLockedEventMsg extends Message { super(); } - validate(): void { } + validate(): void {} route(): string { return Routes.InteractionForeground; @@ -178,6 +203,7 @@ export class VaultLockedEventMsg extends Message { export function initEvents(router: Router): void { router.registerMessage(AccountChangedEventMsg); + router.registerMessage(NetworkChangedEventMsg); router.registerMessage(UpdatedBalancesEventMsg); router.registerMessage(UpdatedStakingEventMsg); router.registerMessage(ProposalsUpdatedEventMsg); @@ -197,6 +223,9 @@ export function initEvents(router: Router): void { new CustomEvent(Events.AccountChanged, { detail: clonedMsg }) ); break; + case NetworkChangedEventMsg: + window.dispatchEvent(new CustomEvent(Events.NetworkChanged)); + break; case TxStartedEvent: window.dispatchEvent( new CustomEvent(Events.TxStarted, { detail: clonedMsg }) diff --git a/apps/extension/src/extension/ExtensionBroadcaster.ts b/apps/extension/src/extension/ExtensionBroadcaster.ts index 76147fba9..975191967 100644 --- a/apps/extension/src/extension/ExtensionBroadcaster.ts +++ b/apps/extension/src/extension/ExtensionBroadcaster.ts @@ -3,6 +3,7 @@ import { KVStore } from "@namada/storage"; import { TabStore, syncTabs } from "background/keyring"; import { AccountChangedEventMsg, + NetworkChangedEventMsg, ProposalsUpdatedEventMsg, TxCompletedEvent, TxStartedEvent, @@ -17,7 +18,7 @@ export class ExtensionBroadcaster { constructor( protected readonly connectedTabsStore: KVStore, protected readonly requester: ExtensionRequester - ) { } + ) {} async startTx(msgId: string, txType: TxType): Promise { await this.sendMsgToTabs(new TxStartedEvent(msgId, txType)); @@ -46,6 +47,10 @@ export class ExtensionBroadcaster { await this.sendMsgToTabs(new AccountChangedEventMsg()); } + async updateNetwork(): Promise { + await this.sendMsgToTabs(new NetworkChangedEventMsg()); + } + async updateProposals(): Promise { await this.sendMsgToTabs(new ProposalsUpdatedEventMsg()); } diff --git a/apps/extension/src/provider/InjectedNamada.ts b/apps/extension/src/provider/InjectedNamada.ts index abf90f8c7..c125fa872 100644 --- a/apps/extension/src/provider/InjectedNamada.ts +++ b/apps/extension/src/provider/InjectedNamada.ts @@ -1,4 +1,5 @@ import { + Chain, DerivedAccount, Namada as INamada, Signer as ISigner, @@ -35,6 +36,10 @@ export class InjectedNamada implements INamada { >("balances", owner); } + public async getChain(): Promise { + return await InjectedProxy.requestMethod("getChain"); + } + public getSigner(): ISigner | undefined { return new Signer(this); } diff --git a/apps/extension/src/provider/Namada.ts b/apps/extension/src/provider/Namada.ts index 77902ee72..67a3b97b1 100644 --- a/apps/extension/src/provider/Namada.ts +++ b/apps/extension/src/provider/Namada.ts @@ -1,13 +1,19 @@ -import { Namada as INamada, DerivedAccount, TxMsgProps } from "@namada/types"; -import { Ports, MessageRequester } from "router"; +import { + Chain, + DerivedAccount, + Namada as INamada, + TxMsgProps, +} from "@namada/types"; +import { MessageRequester, Ports } from "router"; import { - ApproveTxMsg, ApproveConnectInterfaceMsg, - QueryAccountsMsg, + ApproveTxMsg, + CheckDurabilityMsg, FetchAndStoreMaspParamsMsg, + GetChainMsg, HasMaspParamsMsg, - CheckDurabilityMsg, + QueryAccountsMsg, QueryBalancesMsg, QueryDefaultAccountMsg, } from "./messages"; @@ -18,7 +24,7 @@ export class Namada implements INamada { protected readonly requester?: MessageRequester ) {} - public async connect(_chainId?: string): Promise { + public async connect(): Promise { return await this.requester?.sendMessage( Ports.Background, new ApproveConnectInterfaceMsg() @@ -26,6 +32,7 @@ export class Namada implements INamada { } public async accounts( + // TODO: This argument should be removed in the future! _chainId?: string ): Promise { return await this.requester?.sendMessage( @@ -35,6 +42,7 @@ export class Namada implements INamada { } public async defaultAccount( + // TODO: This argument should be removed in the future! _chainId?: string ): Promise { return await this.requester?.sendMessage( @@ -50,6 +58,13 @@ export class Namada implements INamada { ); } + public async getChain(): Promise { + return await this.requester?.sendMessage( + Ports.Background, + new GetChainMsg() + ); + } + public async hasMaspParams(): Promise { return await this.requester?.sendMessage( Ports.Background, diff --git a/apps/extension/src/provider/messages.ts b/apps/extension/src/provider/messages.ts index 3730fa6ef..8f5d28e73 100644 --- a/apps/extension/src/provider/messages.ts +++ b/apps/extension/src/provider/messages.ts @@ -26,7 +26,6 @@ enum MessageType { EncodeRevealPublicKey = "encode-reveal-public-key", GetChain = "get-chain", GetChains = "get-chains", - SuggestChain = "suggest-chain", FetchAndStoreMaspParams = "fetch-and-store-masp-params", HasMaspParams = "has-masp-params", ApproveEthBridgeTransfer = "approve-eth-bridge-transfer", @@ -59,66 +58,16 @@ export class ApproveConnectInterfaceMsg extends Message { } } -export class SuggestChainMsg extends Message { - public static type(): MessageType { - return MessageType.SuggestChain; - } - - constructor(public readonly chain: Chain) { - super(); - } - - validate(): void { - if (!this.chain) { - throw new Error("chain config not set"); - } - } - - route(): string { - return Route.Chains; - } - - type(): MessageType { - return SuggestChainMsg.type(); - } -} - -export class GetChainsMsg extends Message { - public static type(): MessageType { - return MessageType.GetChains; - } - - constructor() { - super(); - } - - validate(): void { - return; - } - - route(): string { - return Route.Chains; - } - - type(): string { - return GetChainsMsg.type(); - } -} - -export class GetChainMsg extends Message { +export class GetChainMsg extends Message { public static type(): MessageType { return MessageType.GetChain; } - constructor(public readonly chainId: string) { + constructor() { super(); } - validate(): void { - if (!this.chainId) { - throw new Error("Chain ID not provided!"); - } - } + validate(): void { } route(): string { return Route.Chains; diff --git a/apps/extension/src/test/init.ts b/apps/extension/src/test/init.ts index 388b35f7d..964b306b2 100644 --- a/apps/extension/src/test/init.ts +++ b/apps/extension/src/test/init.ts @@ -1,4 +1,3 @@ -import { Query, Sdk } from "@namada/shared"; import { KVStore } from "@namada/storage"; import { Chain } from "@namada/types"; @@ -33,6 +32,7 @@ import { } from "../background/approvals"; import { LedgerService } from "background/ledger"; +import { SdkService } from "background/sdk"; import { Namada } from "provider"; // __wasm is not exported in crypto.d.ts so need to use require instead of import @@ -42,7 +42,7 @@ const cryptoMemory = require("@namada/crypto").__wasm.memory; export class KVStoreMock implements KVStore { private storage: { [key: string]: T | null } = {}; - constructor(readonly _prefix: string) {} + constructor(readonly _prefix: string) { } get(key: string): Promise { return new Promise((resolve) => { @@ -86,6 +86,7 @@ export const init = async (): Promise<{ const namadaRouterId = await getNamadaRouterId(extStore); const requester = new ExtensionRequester(messenger, namadaRouterId); const txStore = new KVStoreMock(KVPrefix.LocalStorage); + const chainStore = new KVStoreMock(KVPrefix.LocalStorage); const broadcaster = new ExtensionBroadcaster(connectedTabsStore, requester); const router = new ExtensionRouter( @@ -100,22 +101,19 @@ export const init = async (): Promise<{ extStore ); - const sdk = new Sdk(""); - const query = new Query(""); - const vaultService = new VaultService( iDBStore as KVStore, sessionStore, cryptoMemory ); + const sdkService = new SdkService(chainStore); const keyRingService = new KeyRingService( vaultService, + sdkService, utilityStore, connectedTabsStore, extStore, - sdk, - query, cryptoMemory, requester, broadcaster @@ -123,11 +121,11 @@ export const init = async (): Promise<{ const ledgerService = new LedgerService( keyRingService, + sdkService, iDBStore as KVStore, connectedTabsStore, txStore, revealedPKStore, - sdk, requester, broadcaster ); diff --git a/apps/namada-interface/src/App/App.tsx b/apps/namada-interface/src/App/App.tsx index c9a9f821f..0b23e1a47 100644 --- a/apps/namada-interface/src/App/App.tsx +++ b/apps/namada-interface/src/App/App.tsx @@ -25,7 +25,7 @@ import { import { persistor, store, useAppDispatch, useAppSelector } from "store"; import { Toasts } from "App/Toast"; import { SettingsState } from "slices/settings"; -import { chains, defaultChainId as chainId } from "@namada/chains"; +import { chains, defaultChainId } from "@namada/chains"; import { useIntegration, useUntilIntegrationAttached, @@ -33,6 +33,7 @@ import { import { Outlet } from "react-router-dom"; import { addAccounts, fetchBalances } from "slices/accounts"; import { Account } from "@namada/types"; +import { setChain } from "slices/chain"; export const history = createBrowserHistory({ window }); @@ -67,15 +68,15 @@ function App(): JSX.Element { const { connectedChains } = useAppSelector( (state) => state.settings ); - const chain = chains[chainId]; + const defaultChain = chains[defaultChainId]; - const integration = useIntegration(chainId); + const integration = useIntegration(defaultChainId); useEffect(() => storeColorMode(colorMode), [colorMode]); - const extensionAttachStatus = useUntilIntegrationAttached(chain); + const extensionAttachStatus = useUntilIntegrationAttached(defaultChain); const currentExtensionAttachStatus = - extensionAttachStatus[chain.extension.id]; + extensionAttachStatus[defaultChain.extension.id]; useEffect(() => { const fetchAccounts = async (): Promise => { @@ -87,12 +88,23 @@ function App(): JSX.Element { }; if ( currentExtensionAttachStatus === "attached" && - connectedChains.includes(chainId) + connectedChains.includes(defaultChainId) ) { fetchAccounts(); } }); + useEffect(() => { + (async () => { + if (currentExtensionAttachStatus === "attached") { + const chain = await integration.getChain(); + if (chain) { + dispatch(setChain(chain)); + } + } + })() + }, [currentExtensionAttachStatus]) + return ( diff --git a/apps/namada-interface/src/App/Token/TokenSend/TokenSendForm.tsx b/apps/namada-interface/src/App/Token/TokenSend/TokenSendForm.tsx index c76a89328..60db11337 100644 --- a/apps/namada-interface/src/App/Token/TokenSend/TokenSendForm.tsx +++ b/apps/namada-interface/src/App/Token/TokenSend/TokenSendForm.tsx @@ -4,10 +4,10 @@ import QrReader from "react-qr-reader"; import { useNavigate } from "react-router-dom"; import { ThemeContext } from "styled-components"; -import { defaultChainId as chainId } from "@namada/chains"; +import { defaultChainId } from "@namada/chains"; import { ActionButton, Icon, Input } from "@namada/components"; import { getIntegration } from "@namada/integrations"; -import { Signer, TokenType, Tokens } from "@namada/types"; +import { Chain, Signer, TokenType, Tokens } from "@namada/types"; import { ColorMode, DesignConfiguration } from "@namada/utils"; import { TopLevelRoute } from "App/types"; import { AccountsState } from "slices/accounts"; @@ -57,12 +57,13 @@ export const submitTransferTransaction = async ( const { account: { address, publicKey, type }, amount, + chainId, faucet, target, token, disposableSigningKey, } = txTransferArgs; - const integration = getIntegration(chainId); + const integration = getIntegration(defaultChainId); const signer = integration.signer() as Signer; const transferArgs = { @@ -143,6 +144,7 @@ const TokenSendForm = ( ): JSX.Element => { const navigate = useNavigate(); const themeContext = useContext(ThemeContext); + const chain = useAppSelector((state) => state.chain.config); const [target, setTarget] = useState(defaultTarget); const [amount, setAmount] = useState(new BigNumber(0)); @@ -166,7 +168,7 @@ const TokenSendForm = ( ); const { rates } = useAppSelector((state) => state.coins); const { derived } = useAppSelector((state) => state.accounts); - const derivedAccounts = derived[chainId]; + const derivedAccounts = derived[defaultChainId]; const { details, balance } = derivedAccounts[address]; const isShieldedSource = details.isShielded; @@ -246,8 +248,8 @@ const TokenSendForm = ( const handleOnSendClick = (): void => { if ((isShieldedTarget && target) || (target && token.address)) { submitTransferTransaction({ - chainId, account: details, + chainId: chain.chainId, target, amount, token: tokenType, diff --git a/apps/namada-interface/src/services/extensionEvents/handlers/namada.ts b/apps/namada-interface/src/services/extensionEvents/handlers/namada.ts index d7d0f1d80..502324978 100644 --- a/apps/namada-interface/src/services/extensionEvents/handlers/namada.ts +++ b/apps/namada-interface/src/services/extensionEvents/handlers/namada.ts @@ -5,6 +5,7 @@ import { TxType, TxTypeLabel } from "@namada/shared"; import { fetchValidators } from "slices/StakingAndGovernance/actions"; import { addAccounts, fetchBalances } from "slices/accounts"; +import { setChain } from "slices/chain"; import { actions as notificationsActions } from "slices/notifications"; import { fetchProposals } from "slices/proposals"; @@ -16,6 +17,15 @@ export const NamadaAccountChangedHandler = dispatch(fetchBalances()); }; +export const NamadaNetworkChangedHandler = + (dispatch: Dispatch, integration: Namada) => async () => { + const chain = await integration.getChain(); + if (chain) { + dispatch(setChain(chain)); + dispatch(fetchBalances()); + } + }; + export const NamadaProposalsUpdatedHandler = (dispatch: Dispatch) => async () => { dispatch(fetchProposals()); diff --git a/apps/namada-interface/src/services/extensionEvents/provider.tsx b/apps/namada-interface/src/services/extensionEvents/provider.tsx index af7b113ea..af382a232 100644 --- a/apps/namada-interface/src/services/extensionEvents/provider.tsx +++ b/apps/namada-interface/src/services/extensionEvents/provider.tsx @@ -15,6 +15,7 @@ import { MetamaskAccountChangedHandler, MetamaskBridgeTransferCompletedHandler, NamadaAccountChangedHandler, + NamadaNetworkChangedHandler, NamadaProposalsUpdatedHandler, NamadaTxCompletedHandler, NamadaTxStartedHandler, @@ -35,6 +36,10 @@ export const ExtensionEventsProvider: React.FC = (props): JSX.Element => { dispatch, namadaIntegration as Namada ); + const namadaNetworkChangedHandler = NamadaNetworkChangedHandler( + dispatch, + namadaIntegration as Namada + ); const namadaTxStartedHandler = NamadaTxStartedHandler(dispatch); const namadaTxCompletedHandler = NamadaTxCompletedHandler(dispatch); const namadaUpdatedBalancesHandler = NamadaUpdatedBalancesHandler(dispatch); @@ -58,6 +63,7 @@ export const ExtensionEventsProvider: React.FC = (props): JSX.Element => { // Register handlers: useEventListenerOnce(Events.AccountChanged, namadaAccountChangedHandler); + useEventListenerOnce(Events.NetworkChanged, namadaNetworkChangedHandler); useEventListenerOnce(Events.UpdatedBalances, namadaUpdatedBalancesHandler); useEventListenerOnce(Events.UpdatedStaking, namadaUpdatedStakingHandler); useEventListenerOnce(Events.TxStarted, namadaTxStartedHandler); diff --git a/apps/namada-interface/src/slices/chain.ts b/apps/namada-interface/src/slices/chain.ts new file mode 100644 index 000000000..dd3010d26 --- /dev/null +++ b/apps/namada-interface/src/slices/chain.ts @@ -0,0 +1,26 @@ +import { chains, defaultChainId } from "@namada/chains"; +import { Chain } from "@namada/types"; +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; + +export type ChainState = { + config: Chain; +}; + +const initialState: ChainState = { config: chains[defaultChainId] }; + +const CHAIN_ACTIONS_BASE = "chain"; +const chainSlice = createSlice({ + name: CHAIN_ACTIONS_BASE, + initialState, + reducers: { + setChain: (state, action: PayloadAction) => { + const chain = action.payload; + state.config = chain; + }, + }, +}); + +const { actions, reducer } = chainSlice; +export const { setChain } = actions; + +export default reducer; diff --git a/apps/namada-interface/src/slices/index.ts b/apps/namada-interface/src/slices/index.ts index ebfb551a7..59988ccee 100644 --- a/apps/namada-interface/src/slices/index.ts +++ b/apps/namada-interface/src/slices/index.ts @@ -1,7 +1,8 @@ +export { stakingAndGovernanceReducers } from "./StakingAndGovernance"; export { default as accountsReducer } from "./accounts"; +export { default as chainReducer } from "./chain"; export { default as channelsReducer } from "./channels"; export { default as coinsReducer } from "./coins"; +export { notificationsReducer } from "./notifications"; export { default as proposalsReducers } from "./proposals"; export { default as settingsReducer } from "./settings"; -export { notificationsReducer } from "./notifications"; -export { stakingAndGovernanceReducers } from "./StakingAndGovernance"; diff --git a/apps/namada-interface/src/store/mocks.ts b/apps/namada-interface/src/store/mocks.ts index b8dfa1b9e..fbcc33798 100644 --- a/apps/namada-interface/src/store/mocks.ts +++ b/apps/namada-interface/src/store/mocks.ts @@ -1,5 +1,6 @@ import BigNumber from "bignumber.js"; +import { chains, defaultChainId } from "@namada/chains"; import { AccountType } from "@namada/types"; import { StakingOrUnstakingState } from "slices/StakingAndGovernance"; import { RootState } from "./store"; @@ -48,7 +49,6 @@ export const mockAppState: RootState = { type: AccountType.PrivateKey, isShielded: false, }, - balance: { NAM: new BigNumber(1000), DOT: new BigNumber(1000), @@ -58,6 +58,7 @@ export const mockAppState: RootState = { }, }, }, + chain: { config: chains[defaultChainId] }, channels: { channelsByChain: { "namada-test.1e670ba91369ec891fc": { diff --git a/apps/namada-interface/src/store/store.ts b/apps/namada-interface/src/store/store.ts index d967f1c73..f8e4d68c6 100644 --- a/apps/namada-interface/src/store/store.ts +++ b/apps/namada-interface/src/store/store.ts @@ -6,6 +6,7 @@ import storage from "redux-persist/lib/storage"; import thunk from "redux-thunk"; import { accountsReducer, + chainReducer, channelsReducer, coinsReducer, notificationsReducer, @@ -35,6 +36,7 @@ const ChainIdTransform = createTransform( const reducers = combineReducers({ accounts: accountsReducer || {}, + chain: chainReducer, channels: channelsReducer, settings: settingsReducer, coins: coinsReducer, @@ -47,7 +49,7 @@ const persistConfig = { key: `${LocalStorageKeys.Persist}${POSTFIX}`, storage, // Only persist data in whitelist: - whitelist: ["settings", "channels"], + whitelist: ["settings", "chain", "channels"], transforms: [ChainIdTransform], }; diff --git a/packages/chains/src/chains/namada.ts b/packages/chains/src/chains/namada.ts index 2d54d2b3d..f6be90f49 100644 --- a/packages/chains/src/chains/namada.ts +++ b/packages/chains/src/chains/namada.ts @@ -1,10 +1,9 @@ import { BridgeType, Chain, Extensions } from "@namada/types"; import { ProxyMappings } from "../types"; -const DEFAULT_ALIAS = "Namada Testnet"; -const DEFAULT_CHAIN_ID = "qc-testnet-5.1.025a61165acd05e"; -const DEFAULT_RPC = - "https://d3brk13lbhxfdb.cloudfront.net/qc-testnet-5.1.025a61165acd05e"; +const DEFAULT_ALIAS = "Namada"; +const DEFAULT_CHAIN_ID = "public-testnet-15.0dacadb8d663"; +const DEFAULT_RPC = "https://proxy.heliax.click/public-testnet-15.0dacadb8d663"; const DEFAULT_BECH32_PREFIX = "tnam"; const { diff --git a/packages/integrations/src/Keplr.ts b/packages/integrations/src/Keplr.ts index def084258..4c568da67 100644 --- a/packages/integrations/src/Keplr.ts +++ b/packages/integrations/src/Keplr.ts @@ -101,6 +101,10 @@ class Keplr implements Integration { return Promise.reject(KEPLR_NOT_FOUND); } + public async getChain(): Promise { + return this.chain; + } + /** * Get key from Keplr for current chain * @returns {Promise} diff --git a/packages/integrations/src/Metamask.ts b/packages/integrations/src/Metamask.ts index d0d871691..ad83e4461 100644 --- a/packages/integrations/src/Metamask.ts +++ b/packages/integrations/src/Metamask.ts @@ -31,7 +31,7 @@ declare global { class Metamask implements Integration { private _ethereum: MetaMaskInpageProvider | undefined; - constructor(public readonly chain: Chain) {} + constructor(public readonly chain: Chain) { } private init(): void { const provider = window.ethereum; @@ -81,6 +81,10 @@ class Metamask implements Integration { } } + public async getChain(): Promise { + return this.chain; + } + private async syncChainId(): Promise { const { chainId } = this.chain; diff --git a/packages/integrations/src/Namada.ts b/packages/integrations/src/Namada.ts index b21239851..fa160a9b4 100644 --- a/packages/integrations/src/Namada.ts +++ b/packages/integrations/src/Namada.ts @@ -1,13 +1,13 @@ import { Account, - Namada as INamada, + AccountType, Chain, + Namada as INamada, Signer, - Tokens, - TokenType, TokenBalance, + TokenType, + Tokens, WindowWithNamada, - AccountType, } from "@namada/types"; import BigNumber from "bignumber.js"; @@ -16,7 +16,7 @@ import { BridgeProps, Integration } from "./types/Integration"; export default class Namada implements Integration { private _namada: WindowWithNamada["namada"] | undefined; - constructor(public readonly chain: Chain) {} + constructor(public readonly chain: Chain) { } public get instance(): INamada | undefined { return this._namada; @@ -37,6 +37,10 @@ export default class Namada implements Integration { await this._namada?.connect(chainId); } + public async getChain(): Promise { + return await this._namada?.getChain(); + } + public async accounts( chainId?: string ): Promise { diff --git a/packages/integrations/src/types/Integration.ts b/packages/integrations/src/types/Integration.ts index 77bc35cc3..f9fe61654 100644 --- a/packages/integrations/src/types/Integration.ts +++ b/packages/integrations/src/types/Integration.ts @@ -1,6 +1,7 @@ import { AccountType, BridgeTransferProps, + Chain, IbcTransferProps, TokenBalance, TxProps, @@ -14,8 +15,9 @@ export type BridgeProps = { export interface Integration { detect: () => boolean; - connect: () => Promise; + connect: (chainId: string) => Promise; accounts: () => Promise; + getChain?: () => Promise; signer: () => S | undefined; submitBridgeTransfer: ( props: BridgeProps, diff --git a/packages/types/src/events.ts b/packages/types/src/events.ts index 1e75366c0..b4dc00606 100644 --- a/packages/types/src/events.ts +++ b/packages/types/src/events.ts @@ -3,6 +3,7 @@ // Namada extension events export enum Events { AccountChanged = "namada-account-changed", + NetworkChanged = "namada-network-changed", TxStarted = "namada-tx-started", TxCompleted = "namada-tx-completed", UpdatedBalances = "namada-updated-balances", diff --git a/packages/types/src/namada.ts b/packages/types/src/namada.ts index 16a7b8f2c..00fbfa5ce 100644 --- a/packages/types/src/namada.ts +++ b/packages/types/src/namada.ts @@ -1,4 +1,5 @@ import { AccountType, DerivedAccount } from "./account"; +import { Chain } from "./chain"; import { Signer } from "./signer"; export type TxMsgProps = { @@ -11,13 +12,14 @@ export type TxMsgProps = { }; export interface Namada { - connect(chainId?: string): Promise; accounts(chainId?: string): Promise; - defaultAccount(chainId?: string): Promise; balances( owner: string ): Promise<{ token: string; amount: string }[] | undefined>; + connect(chainId?: string): Promise; + defaultAccount(chainId?: string): Promise; submitTx: (props: TxMsgProps) => Promise; + getChain: () => Promise; version: () => string; } diff --git a/packages/utils/src/helpers/index.ts b/packages/utils/src/helpers/index.ts index 32cc13e7c..73e83aa2b 100644 --- a/packages/utils/src/helpers/index.ts +++ b/packages/utils/src/helpers/index.ts @@ -291,3 +291,18 @@ export const matchMapFn = ( return; } }; + +/** + * Check that an input url is valid + * + * @param {string} url + * @returns {boolean} + */ +export const isUrlValid = (url: string): boolean => { + try { + const newUrl = new URL(url); + return newUrl.protocol === "http:" || newUrl.protocol === "https:"; + } catch (err) { + return false; + } +};