diff --git a/components/app/Shared/Buttons/SyncViewingKeyButton.tsx b/components/app/Shared/Buttons/SyncViewingKeyButton.tsx index 8e70b84..33978ee 100644 --- a/components/app/Shared/Buttons/SyncViewingKeyButton.tsx +++ b/components/app/Shared/Buttons/SyncViewingKeyButton.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { useViewingKeyStore } from "@/store/viewingKeyStore"; +import { useViewingKeyStore } from "@/store/viewingKeyStore2"; import { SecretString } from "@/types"; const SyncViewingKeyButton = ({ diff --git a/components/app/Testing/SelectComponent2.tsx b/components/app/Testing/SelectComponent2.tsx index f9c5349..e8d528c 100644 --- a/components/app/Testing/SelectComponent2.tsx +++ b/components/app/Testing/SelectComponent2.tsx @@ -1,24 +1,25 @@ import React, { useState, useEffect } from "react"; import fullPoolsData from "@/outputs/fullPoolsData.json"; import { fetchTokenData, getTokenName } from "@/utils/apis/tokenInfo"; +import { SecretString } from "@/types"; interface SelectComponentProps { apiUrl?: string; - setFrom?: (from: string) => void; - setTo?: (to: string) => void; - outputOptions?: string[]; // New prop for filtered output options + setFrom?: (from: SecretString | "") => void; + setTo?: (to: SecretString | "") => void; + outputOptions?: SecretString[]; } const SelectComponent2: React.FC = ({ apiUrl = "/api/tokens", setFrom, setTo, - outputOptions = [], // Default to empty array if not provided + outputOptions = [] as SecretString[], }) => { - const [fromTokens, setFromTokens] = useState([]); - const [toTokens, setToTokens] = useState([]); - const [selectedFrom, setSelectedFrom] = useState(""); - const [selectedTo, setSelectedTo] = useState(""); + const [fromTokens, setFromTokens] = useState([]); + const [toTokens, setToTokens] = useState([]); + const [selectedFrom, setSelectedFrom] = useState(""); + const [selectedTo, setSelectedTo] = useState(""); const [tokenNames, setTokenNames] = useState<{ [key: string]: string }>({}); useEffect(() => { @@ -59,7 +60,7 @@ const SelectComponent2: React.FC = ({ (addressOrDenom): addressOrDenom is string => addressOrDenom !== undefined ) - .filter((address) => address !== "uscrt"); + .filter((address) => address !== "uscrt") as SecretString[]; setFromTokens(Array.from(new Set(fromOptions))); }; @@ -68,7 +69,7 @@ const SelectComponent2: React.FC = ({ }, [apiUrl]); const handleFromSelect = (event: React.ChangeEvent) => { - const fromToken = event.target.value; + const fromToken = event.target.value as SecretString; setSelectedFrom(fromToken); if (outputOptions.length > 0) { @@ -90,7 +91,7 @@ const SelectComponent2: React.FC = ({ }) ) .filter((addr): addr is string => addr !== null) - .filter((addr) => addr !== "uscrt"); + .filter((addr) => addr !== "uscrt") as SecretString[]; setToTokens(Array.from(new Set(toOptions))); } @@ -99,7 +100,7 @@ const SelectComponent2: React.FC = ({ }; const handleToSelect = (event: React.ChangeEvent) => { - setSelectedTo(event.target.value); + setSelectedTo(event.target.value as SecretString); }; return ( diff --git a/components/app/Testing/ViewingKeyModal.tsx b/components/app/Testing/ViewingKeyModal.tsx new file mode 100644 index 0000000..b27d425 --- /dev/null +++ b/components/app/Testing/ViewingKeyModal.tsx @@ -0,0 +1,329 @@ +import React, { useState, useEffect } from "react"; +import { useViewingKeyStore } from "@/store/viewingKeyStore"; +import { SecretString } from "@/types"; +import { Keplr, Window as KeplrWindow } from "@keplr-wallet/types"; +import { FiCheckCircle, FiX, FiArrowLeft } from "react-icons/fi"; +import { Tooltip } from "react-tooltip"; +import "react-tooltip/dist/react-tooltip.css"; + +// Reusable component for Registration and Syncing Buttons +const TokenActionButton: React.FC<{ + action: () => void; + isActionCompleted: boolean; + actionText: string; + completedText: string; + disabled: boolean; + tooltipId: string; + tooltipContent: string; +}> = ({ + action, + isActionCompleted, + actionText, + completedText, + disabled, + tooltipId, + tooltipContent, +}) => ( + <> + + + +); + +// Helper function to check sync status +const checkSyncStatus = async ( + tokenAddress: SecretString, + setRegistered: React.Dispatch>, + setSynced: React.Dispatch>, + getViewingKey: (address: string) => string | undefined +) => { + try { + const chainId = process.env.NEXT_PUBLIC_CHAIN_ID!; + if (!window.keplr) return; + const viewingKey = await ( + window.keplr as unknown as Keplr + ).getSecret20ViewingKey(chainId, tokenAddress); + + if (viewingKey) { + setRegistered(true); + const storedKey = getViewingKey(tokenAddress); + if (storedKey === viewingKey) { + setSynced(true); + } + } + } catch (error) { + console.error("Error checking sync status:", error); + setRegistered(false); + } +}; + +interface ViewingKeyModalProps { + tokenIn: SecretString; + tokenOut: SecretString; + onClose: () => void; +} + +// Main Component +const ViewingKeyModal: React.FC = ({ + tokenIn, + tokenOut, + onClose, +}) => { + const { setViewingKey, getViewingKey } = useViewingKeyStore(); + const [customKey, setCustomKey] = useState(""); + const [showCustomKeyField, setShowCustomKeyField] = useState(false); + const [isInputRegistered, setIsInputRegistered] = useState(false); + const [isOutputRegistered, setIsOutputRegistered] = useState(false); + const [inputKeySynced, setInputKeySynced] = useState(false); + const [outputKeySynced, setOutputKeySynced] = useState(false); + + useEffect(() => { + checkSyncStatus( + tokenIn, + setIsInputRegistered, + setInputKeySynced, + getViewingKey + ); + checkSyncStatus( + tokenOut, + setIsOutputRegistered, + setOutputKeySynced, + getViewingKey + ); + }, [tokenIn, tokenOut, getViewingKey]); + + const handleRegisterToken = async ( + tokenAddress: SecretString, + setRegistered: React.Dispatch> + ) => { + try { + if (!(window as unknown as KeplrWindow).keplr) { + alert("Keplr extension not detected."); + return; + } + if (!process.env.NEXT_PUBLIC_CHAIN_ID) { + alert("Chain ID not set in environment."); + return; + } + + await (window as unknown as KeplrWindow).keplr!.suggestToken( + process.env.NEXT_PUBLIC_CHAIN_ID, + tokenAddress + ); + setRegistered(true); + alert("Token registration requested."); + } catch (error) { + console.error("Error registering token with Keplr:", error); + alert("Failed to register token."); + } + }; + + const handleSyncViewingKey = async ( + tokenAddress: SecretString, + setSynced: React.Dispatch> + ) => { + try { + if (!window.keplr) { + alert("Keplr extension not detected."); + return; + } + + if (!process.env.NEXT_PUBLIC_CHAIN_ID) { + alert("Chain ID not set in environment."); + return; + } + + const chainId = process.env.NEXT_PUBLIC_CHAIN_ID!; + const viewingKey = await ( + window.keplr as unknown as Keplr + ).getSecret20ViewingKey(chainId, tokenAddress); + + setViewingKey(tokenAddress, viewingKey); + setSynced(true); + alert("Viewing key synchronized successfully."); + } catch (error) { + console.error("Error fetching viewing key:", error); + alert( + "Failed to sync the viewing key. Make sure the token is registered in Keplr." + ); + } + }; + + const handleSubmitCustomKey = ( + tokenAddress: string, + setSynced: React.Dispatch> + ) => { + if (customKey) { + setViewingKey(tokenAddress, customKey); + setCustomKey(""); + setShowCustomKeyField(false); + setSynced(true); + alert("Viewing key registered successfully."); + } + }; + + return ( +
+
+ {/* Close Button */} + + + {/* Modal Header */} +

+ Viewing Key Dashboard +

+ + {!showCustomKeyField ? ( + <> + {/* Input Token Section */} +
+

+ Input Token +

+ + handleRegisterToken(tokenIn, setIsInputRegistered) + } + isActionCompleted={isInputRegistered} + actionText="Register Input Token" + completedText="Input Token Registered" + disabled={inputKeySynced} + tooltipId="inputRegisterTip" + tooltipContent={ + isInputRegistered + ? "This token is already registered." + : "Click to register the input token in Keplr." + } + /> + handleSyncViewingKey(tokenIn, setInputKeySynced)} + isActionCompleted={inputKeySynced} + actionText="Sync Input Token Key" + completedText="Input Token Key Synced" + disabled={!isInputRegistered} + tooltipId="inputSyncTip" + tooltipContent={ + inputKeySynced + ? "Viewing key is already synced." + : "Click to sync the input token's viewing key." + } + /> +
+ + {/* Output Token Section */} +
+

+ Output Token +

+ + handleRegisterToken(tokenOut, setIsOutputRegistered) + } + isActionCompleted={isOutputRegistered} + actionText="Register Output Token" + completedText="Output Token Registered" + disabled={outputKeySynced} + tooltipId="outputRegisterTip" + tooltipContent={ + isOutputRegistered + ? "This token is already registered." + : "Click to register the output token in Keplr." + } + /> + + handleSyncViewingKey(tokenOut, setOutputKeySynced) + } + isActionCompleted={outputKeySynced} + actionText="Sync Output Token Key" + completedText="Output Token Key Synced" + disabled={!isOutputRegistered} + tooltipId="outputSyncTip" + tooltipContent={ + outputKeySynced + ? "Viewing key is already synced." + : "Click to sync the output token's viewing key." + } + /> +
+ + {/* Custom Key Section */} +
+

+ If you have a custom viewing key, you can manually enter it + here. +

+ +
+ + ) : ( + <> + {/* Back Button */} + + + {/* Custom Key Input Section */} +
+

+ Enter Your Viewing Key +

+ setCustomKey(e.target.value)} + placeholder="Paste your viewing key" + className="px-4 py-2 border border-adamant-box-border bg-adamant-app-input rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-adamant-accentBg text-white w-full mb-4" + /> + + +
+ + )} +
+
+ ); +}; + +export default ViewingKeyModal; diff --git a/pages/app/testing/estimate/a.tsx b/pages/app/testing/estimate/a.tsx index 736ee1a..82f60fd 100644 --- a/pages/app/testing/estimate/a.tsx +++ b/pages/app/testing/estimate/a.tsx @@ -1,11 +1,14 @@ import { Window as KeplrWindow } from "@keplr-wallet/types"; import { useState, useEffect } from "react"; -import { SecretNetworkClient } from "secretjs"; +import { SecretNetworkClient, TxOptions } from "secretjs"; import Decimal from "decimal.js"; import { getTokenDecimals, getTokenName } from "@/utils/apis/tokenInfo"; import { fullPoolsData } from "../../../../components/app/Testing/fullPoolsData"; import SelectComponent2 from "@/components/app/Testing/SelectComponent2"; import SwapResult from "@/components/app/Testing/SwapResult"; +import ViewingKeyModal from "@/components/app/Testing/ViewingKeyModal"; +import { useViewingKeyStore } from "@/store/viewingKeyStore"; +import { SecretString } from "@/types"; interface PoolQueryResponse { assets: { @@ -97,7 +100,7 @@ const findAllReachableTokens = ( return reachableTokens; }; -interface Path { +export interface Path { pools: string[]; // Array of pool addresses tokens: string[]; // Array of token addresses in the path } @@ -145,7 +148,7 @@ const findPaths = ( return paths; }; -interface PathEstimation { +export interface PathEstimation { path: Path; finalOutput: Decimal; totalPriceImpact: string; @@ -416,20 +419,28 @@ const SwapPage = () => { const [amountIn, setAmountIn] = useState(""); const [estimatedOutput, setEstimatedOutput] = useState(""); const [secretjs, setSecretjs] = useState(null); - const [inputToken, setInputToken] = useState(""); - const [outputToken, setOutputToken] = useState(""); - const [outputOptions, setOutputOptions] = useState([]); + const [inputToken, setInputToken] = useState(""); + const [outputToken, setOutputToken] = useState(""); + const [outputOptions, setOutputOptions] = useState([]); const [bestPathEstimation, setBestPathEstimation] = useState(null); + const [walletAddress, setWalletAddress] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const inputViewingKey = useViewingKeyStore((state) => + state.getViewingKey(inputToken) + ); + const outputViewingKey = useViewingKeyStore((state) => + state.getViewingKey(outputToken) + ); useEffect(() => { - // setEstimatedOutput(""); + setEstimatedOutput(""); setBestPathEstimation(null); if (inputToken) { const possibleOutputs = getPossibleOutputsForToken( inputToken, fullPoolsData - ); + ) as SecretString[]; setOutputOptions(possibleOutputs); } }, [inputToken, outputToken]); @@ -459,6 +470,7 @@ const SwapPage = () => { wallet: offlineSigner, walletAddress: accounts[0].address, }); + setWalletAddress(accounts[0].address); setSecretjs(client); }; @@ -466,7 +478,13 @@ const SwapPage = () => { connectKeplr(); }, []); - const handleSwap = async () => { + const handleSyncViewingKeys = () => { + setIsModalOpen(true); + }; + + // first, we estimate the full swap details + + const handleEstimate = async () => { if (secretjs && amountIn && inputToken && outputToken) { const amountInDecimal = new Decimal(amountIn); const tokenPoolMap = buildTokenPoolMap(fullPoolsData); @@ -484,14 +502,14 @@ const SwapPage = () => { ); if (bestPathEstimation) { - console.log("--- Best Path Estimation in handleSwap ---"); + console.log("--- Best Path Estimation in handleEstimate ---"); console.log("Best Path Estimation:", bestPathEstimation); console.log("Final Output:", bestPathEstimation.finalOutput.toString()); console.log("Ideal Output:", bestPathEstimation.idealOutput.toString()); console.log("LP Fee:", bestPathEstimation.totalLpFee.toString()); console.log("Total Price Impact:", bestPathEstimation.totalPriceImpact); console.log("Total Gas Cost:", bestPathEstimation.totalGasCost); - console.log("--- End Best Path Estimation in handleSwap ---"); + console.log("--- End Best Path Estimation in handleEstimate ---"); setBestPathEstimation(bestPathEstimation); } else { @@ -500,6 +518,131 @@ const SwapPage = () => { } }; + // then, we allow the user to execute the swap + + // function getViewingKey(tokenAddress: string): string | undefined { + // return useViewingKeyStore.getState().getViewingKey(tokenAddress); + // } + + const handleSwap = async () => { + if (!secretjs) { + console.error("SecretNetworkClient is not initialized"); + return; + } + + const path = bestPathEstimation?.path; + if (!path) { + console.error("No path found for swap execution"); + return; + } + + if (!inputViewingKey || !outputViewingKey) { + alert("Viewing keys are missing. Please sync them before swapping."); + return; + } + + try { + // Fetch the account information to get the sequence number and account number + const accountInfo = await secretjs.query.auth.account({ + address: walletAddress!, + }); + + const baseAccount = accountInfo as { + "@type": "/cosmos.auth.v1beta1.BaseAccount"; + sequence?: string; + account_number?: string; + }; + + // Check if sequence number is available + const sequence = baseAccount.sequence + ? parseInt(baseAccount.sequence, 10) + : null; + const accountNumber = baseAccount.account_number + ? parseInt(baseAccount.account_number, 10) + : null; + + for (let i = 0; i < path.pools.length; i++) { + const poolAddress = path.pools[i]; + const inputToken = path.tokens[i]; + const outputToken = path.tokens[i + 1]; + + console.log(`Executing swap ${i + 1} on pool ${poolAddress}`); + console.log(`Swapping ${inputToken} for ${outputToken}`); + + const decimals = getTokenDecimals(inputToken); + if (decimals === undefined) { + throw new Error( + `Decimals for token ${inputToken} could not be determined` + ); + } + + const swapMsg = { + swap: { + offer_asset: { + info: { + token: { + contract_addr: inputToken, + token_code_hash: + "0dfd06c7c3c482c14d36ba9826b83d164003f2b0bb302f222db72361e0927490", + viewing_key: inputViewingKey, + }, + }, + amount: bestPathEstimation.finalOutput + .mul(Decimal.pow(10, decimals)) + .toFixed(0), + }, + belief_price: "0", + max_spread: "0.5", + to: walletAddress, + }, + }; + + console.log("Swapping with message:", JSON.stringify(swapMsg, null, 2)); + + const txOptions: TxOptions = { + gasLimit: 200_000, + gasPriceInFeeDenom: 0.25, + feeDenom: "uscrt", + explicitSignerData: + sequence !== null && accountNumber !== null + ? { + accountNumber: accountNumber, + sequence: sequence + i, // Handle sequence increment only if available + chainId: "secret-4", // Replace with your actual chain ID + } + : undefined, + }; + + const result = await secretjs.tx.compute.executeContract( + { + sender: walletAddress!, + contract_address: poolAddress, + code_hash: + "0dfd06c7c3c482c14d36ba9826b83d164003f2b0bb302f222db72361e0927490", + msg: swapMsg, + sent_funds: [], + }, + txOptions + ); + + console.log("Transaction Result:", result); + + if (result.code !== 0) { + throw new Error(`Swap failed at step ${i + 1}: ${result.rawLog}`); + } + + console.log(`Swap ${i + 1} executed successfully!`); + } + + alert("Swap completed successfully!"); + setEstimatedOutput("Swap completed successfully!"); + } catch (error) { + console.error("Error during swap execution:", error); + alert("Swap failed. Check the console for more details."); + setEstimatedOutput("Error during swap execution. Please try again."); + } + }; + return (
@@ -522,11 +665,12 @@ const SwapPage = () => { className="px-4 py-2 border border-gray-700 bg-adamant-app-input rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-adamant-accentBg text-white" /> + {bestPathEstimation && ( // prettier-ignore { )}

{estimatedOutput}

+ {bestPathEstimation && ( + <> + + {inputViewingKey && outputViewingKey ? ( + + ) : ( +

+ Viewing keys are required to execute the swap. Sync them + first. +

+ )} + + )}
+ {isModalOpen && inputToken !== "" && outputToken !== "" && ( + setIsModalOpen(false)} + // secretjs={secretjs!} + // walletAddress={walletAddress!} + /> + )} ); }; diff --git a/store/viewingKeyStore.ts b/store/viewingKeyStore.ts index 397372d..c202307 100644 --- a/store/viewingKeyStore.ts +++ b/store/viewingKeyStore.ts @@ -1,29 +1,31 @@ import { create } from "zustand"; +import { persist, createJSONStorage } from "zustand/middleware"; -interface ViewingKeyStoreState { - viewingKeys: Record; // Maps token address to viewing key - setViewingKey: (address: string, key: string) => void; - getViewingKey: (address: string) => string | undefined; - removeAllViewingKeys: () => void; +interface ViewingKeyStore { + viewingKeys: { [tokenAddress: string]: string }; + setViewingKey: (tokenAddress: string, viewingKey: string) => void; + getViewingKey: (tokenAddress: string) => string | undefined; } -export const useViewingKeyStore = create((set, get) => ({ - viewingKeys: {}, - - setViewingKey: (address, key) => { - set((state) => ({ - viewingKeys: { - ...state.viewingKeys, - [address]: key, +export const useViewingKeyStore = create()( + persist( + (set, get) => ({ + viewingKeys: {}, + setViewingKey: (tokenAddress: string, viewingKey: string) => { + set((state) => ({ + viewingKeys: { + ...state.viewingKeys, + [tokenAddress]: viewingKey, + }, + })); }, - })); - }, - - getViewingKey: (address) => { - return get().viewingKeys[address]; - }, - - removeAllViewingKeys: () => { - set({ viewingKeys: {} }); - }, -})); + getViewingKey: (tokenAddress: string) => { + return get().viewingKeys[tokenAddress]; + }, + }), + { + name: "viewing-key-store", + storage: createJSONStorage(() => sessionStorage), + } + ) +); diff --git a/store/viewingKeyStore2.ts b/store/viewingKeyStore2.ts new file mode 100644 index 0000000..397372d --- /dev/null +++ b/store/viewingKeyStore2.ts @@ -0,0 +1,29 @@ +import { create } from "zustand"; + +interface ViewingKeyStoreState { + viewingKeys: Record; // Maps token address to viewing key + setViewingKey: (address: string, key: string) => void; + getViewingKey: (address: string) => string | undefined; + removeAllViewingKeys: () => void; +} + +export const useViewingKeyStore = create((set, get) => ({ + viewingKeys: {}, + + setViewingKey: (address, key) => { + set((state) => ({ + viewingKeys: { + ...state.viewingKeys, + [address]: key, + }, + })); + }, + + getViewingKey: (address) => { + return get().viewingKeys[address]; + }, + + removeAllViewingKeys: () => { + set({ viewingKeys: {} }); + }, +})); diff --git a/types/ExperimentalKeplrType.ts b/types/ExperimentalKeplrType.ts index 1411754..ca8d037 100644 --- a/types/ExperimentalKeplrType.ts +++ b/types/ExperimentalKeplrType.ts @@ -11,6 +11,7 @@ interface ExperimentalKeplrType { suggestChain(chainInfo: ChainInfo): Promise; experimentalSuggestChain(chainInfo: ChainInfo): Promise; getKey(chainId: string): Promise; + getRegisteredSecret20Tokens(chainId: string): Promise; } // interface OfflineSigner { diff --git a/utils/apis/tokenInfo.ts b/utils/apis/tokenInfo.ts index dca517f..5456640 100644 --- a/utils/apis/tokenInfo.ts +++ b/utils/apis/tokenInfo.ts @@ -52,5 +52,11 @@ export const getTokenName = (address: string): string | undefined => { // Get token decimals by address export const getTokenDecimals = (address: string): number | undefined => { + if (address === "uscrt") { + return 6; + } + if (address === "secret1k0jntykt7e4g3y88ltc60czgjuqdy4c9e8fzek") { + return 6; + } return tokenData.get(address)?.decimals; };