diff --git a/example/next/pages/_app.tsx b/example/next/pages/_app.tsx index 9f595bea..dccb9589 100644 --- a/example/next/pages/_app.tsx +++ b/example/next/pages/_app.tsx @@ -1,5 +1,4 @@ import { ChakraProvider, extendTheme } from "@chakra-ui/react"; -import { CapsuleEnvironment } from "@leapwallet/cosmos-social-login-capsule-provider"; import { GrazProvider } from "graz"; import type { NextPage } from "next"; import type { AppProps } from "next/app"; @@ -24,7 +23,7 @@ const CustomApp: NextPage = ({ Component, pageProps }) => { }, capsuleConfig: { apiKey: "72c07c099c0f3d8e744bb0754a11726b", - env: CapsuleEnvironment.BETA, + env: "BETA", }, }} > diff --git a/example/next/pages/index.tsx b/example/next/pages/index.tsx index f2228e95..cc23a7ad 100644 --- a/example/next/pages/index.tsx +++ b/example/next/pages/index.tsx @@ -17,7 +17,7 @@ const HomePage: NextPage = () => { const { data: accountData } = useAccount({ chainId: "cosmoshub-4", }); - const { client, modalState, onSuccessfulLogin, setModalState } = useCapsule(); + const { client, modalState, onAfterLoginSuccessful, setModalState, onLoginFailure } = useCapsule(); const { colorMode } = useColorMode(); return (
@@ -47,11 +47,10 @@ const HomePage: NextPage = () => { { - void onSuccessfulLogin?.(); - console.log("login successful"); + void onAfterLoginSuccessful?.(); }} onLoginFailure={() => { - console.log("login failure"); + onLoginFailure(); }} setShowCapsuleModal={setModalState} showCapsuleModal={modalState} diff --git a/example/starter/public/assets/wallet-icon-capsule.jpg b/example/starter/public/assets/wallet-icon-capsule.jpg new file mode 100644 index 00000000..2703ea0e Binary files /dev/null and b/example/starter/public/assets/wallet-icon-capsule.jpg differ diff --git a/example/starter/src/pages/_app.tsx b/example/starter/src/pages/_app.tsx index fbd0fb7a..e9fb58c6 100644 --- a/example/starter/src/pages/_app.tsx +++ b/example/starter/src/pages/_app.tsx @@ -29,6 +29,15 @@ const MyApp = ({ Component, pageProps }: AppProps) => { projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, }, }, + capsuleConfig: { + apiKey: "72c07c099c0f3d8e744bb0754a11726b", + env: "BETA", + }, + walletDefaultOptions: { + sign: { + preferNoSetFee: true, + }, + }, }} > diff --git a/example/starter/src/pages/index.tsx b/example/starter/src/pages/index.tsx index 0165b88c..e96bbb4b 100644 --- a/example/starter/src/pages/index.tsx +++ b/example/starter/src/pages/index.tsx @@ -1,14 +1,38 @@ -import { Stack } from "@chakra-ui/react"; +import { Stack, useColorMode } from "@chakra-ui/react"; +import { useCapsule } from "graz"; +import dynamic from "next/dynamic"; import { Card } from "src/ui/card/chain"; import { mainnetChains } from "src/utils/graz"; +const LeapSocialLogin = dynamic( + () => import("@leapwallet/cosmos-social-login-capsule-provider-ui").then((m) => m.CustomCapsuleModalView), + { ssr: false }, +); + const HomePage = () => { + const { client, modalState, onAfterLoginSuccessful, setModalState, onLoginFailure } = useCapsule(); + + const { colorMode } = useColorMode(); return ( - - {mainnetChains.map((chain) => ( - - ))} - + <> + + {mainnetChains.map((chain) => ( + + ))} + + { + void onAfterLoginSuccessful?.(); + }} + onLoginFailure={() => { + onLoginFailure(); + }} + setShowCapsuleModal={setModalState} + showCapsuleModal={modalState} + theme={colorMode} + /> + ); }; diff --git a/example/starter/src/ui/modal/connect-wallet.tsx b/example/starter/src/ui/modal/connect-wallet.tsx index 7b3ba4b7..77097042 100644 --- a/example/starter/src/ui/modal/connect-wallet.tsx +++ b/example/starter/src/ui/modal/connect-wallet.tsx @@ -2,6 +2,7 @@ import { Button, Heading, HStack, + Image, Modal, ModalBody, ModalContent, @@ -52,7 +53,7 @@ const WalletModal = ({ p={4} spacing={4} > - {/* {wallet.name} */} + {wallet.name} {wallet.name} ))} diff --git a/example/starter/src/utils/graz.ts b/example/starter/src/utils/graz.ts index 5433c120..9e993b12 100644 --- a/example/starter/src/utils/graz.ts +++ b/example/starter/src/utils/graz.ts @@ -57,4 +57,8 @@ export const listedWallets = { imgSrc: "/assets/wallet-icon-cosmostation.png", mobile: true, }, + [WalletType.CAPSULE]: { + name: "Capsule", + imgSrc: "/assets/wallet-icon-capsule.jpg", + }, }; diff --git a/packages/graz/package.json b/packages/graz/package.json index c90843ad..bff61d32 100644 --- a/packages/graz/package.json +++ b/packages/graz/package.json @@ -49,7 +49,7 @@ "@cosmjs/tendermint-rpc": "*", "@leapwallet/cosmos-social-login-capsule-provider": "^0.0.30", "@leapwallet/cosmos-social-login-capsule-provider-ui": "^0.0.47", - "long": "*", + "long": "^4", "react": ">=17" }, "peerDependenciesMeta": { @@ -72,7 +72,7 @@ "cac": "^6.7.14", "cosmos-directory-client": "0.0.6", "wadesta": "^0.0.5", - "zustand": "^4.4.1" + "zustand": "^4.5.2" }, "devDependencies": { "@types/node": "^18.17.15", diff --git a/packages/graz/src/actions/account.ts b/packages/graz/src/actions/account.ts index 64982b13..9853b377 100644 --- a/packages/graz/src/actions/account.ts +++ b/packages/graz/src/actions/account.ts @@ -26,14 +26,12 @@ export const connect = async (args?: ConnectArgs): Promise => { const { recentChainIds: recentChains, chains, walletType } = useGrazInternalStore.getState(); const currentWalletType = args?.walletType || walletType; - const isWalletAvailable = checkWallet(currentWalletType); if (!isWalletAvailable) { throw new Error(`${currentWalletType} is not available`); } const wallet = getWallet(currentWalletType); - const chainIds = typeof args?.chainId === "string" ? [args.chainId] : args?.chainId || recentChains; if (!chainIds) { throw new Error("No last known connected chain, connect action requires chain ids"); @@ -60,10 +58,51 @@ export const connect = async (args?: ConnectArgs): Promise => { const { accounts: _account } = useGrazSessionStore.getState(); await wallet.init?.(); + if ( + isCapsule(currentWalletType) && + useGrazSessionStore.getState().capsuleClient && + Object.values(useGrazSessionStore.getState().accounts || []).length > 0 + ) { + const connectedChains = chainIds.map((x) => chains!.find((y) => y.chainId === x)!); + const _resAcc = useGrazSessionStore.getState().accounts; + useGrazSessionStore.setState({ status: "connecting" }); + + const key = await wallet.getKey(chainIds[0]!); + const resultAcccounts: Record = {}; + chainIds.forEach((chainId) => { + resultAcccounts[chainId] = { + ...key, + bech32Address: toBech32( + chains!.find((x) => x.chainId === chainId)!.bech32Config.bech32PrefixAccAddr, + fromBech32(key.bech32Address).data, + ), + }; + }); + useGrazSessionStore.setState((prev) => ({ + accounts: { ...(prev.accounts || {}), ...resultAcccounts }, + })); + + useGrazInternalStore.setState((prev) => ({ + recentChainIds: [...(prev.recentChainIds || []), ...chainIds].filter((thing, i, arr) => { + return arr.indexOf(thing) === i; + }), + })); + useGrazSessionStore.setState((prev) => ({ + activeChainIds: [...(prev.activeChainIds || []), ...chainIds].filter((thing, i, arr) => { + return arr.indexOf(thing) === i; + }), + })); + useGrazSessionStore.setState({ + status: "connected", + }); + return { accounts: _resAcc!, walletType: currentWalletType, chains: connectedChains }; + } await wallet.enable(chainIds); if (isCapsule(currentWalletType)) { - // ignore the return value - throw new Error("CAPSULE_OPEN_MODAL"); + const connectedChains = chainIds.map((x) => chains!.find((y) => y.chainId === x)!); + const _resAcc = useGrazSessionStore.getState().accounts; + useGrazSessionStore.setState({ status: "connecting" }); + return { accounts: _resAcc!, walletType: currentWalletType, chains: connectedChains }; } if (!isWalletConnect(currentWalletType)) { const key = await wallet.getKey(chainIds[0]!); diff --git a/packages/graz/src/actions/chains.ts b/packages/graz/src/actions/chains.ts index 37ab8474..5ca4e8f4 100644 --- a/packages/graz/src/actions/chains.ts +++ b/packages/graz/src/actions/chains.ts @@ -1,7 +1,7 @@ import type { ChainInfo } from "@keplr-wallet/types"; import { useGrazInternalStore } from "../store"; -import type { WalletType } from "../types/wallet"; +import { WalletType } from "../types/wallet"; import type { ConnectResult } from "./account"; import { connect } from "./account"; import { getWallet } from "./wallet"; @@ -34,7 +34,11 @@ export interface SuggestChainArgs { export const suggestChain = async ({ chainInfo, walletType }: SuggestChainArgs): Promise => { const wallet = getWallet(walletType); - await wallet.experimentalSuggestChain(chainInfo); + if (walletType === WalletType.CAPSULE) { + await connect({ chainId: chainInfo.chainId, walletType }); + } else { + await wallet.experimentalSuggestChain(chainInfo); + } return chainInfo; }; diff --git a/packages/graz/src/actions/wallet/capsule.ts b/packages/graz/src/actions/wallet/capsule.ts index 94446db8..d0f015a0 100644 --- a/packages/graz/src/actions/wallet/capsule.ts +++ b/packages/graz/src/actions/wallet/capsule.ts @@ -1,9 +1,11 @@ +import type { AminoSignResponse } from "@cosmjs/amino"; import { fromBech32, toBech32 } from "@cosmjs/encoding"; -import type { Key } from "@keplr-wallet/types"; +import type { DirectSignResponse } from "@cosmjs/proto-signing"; +import type { Keplr, Key } from "@keplr-wallet/types"; import { RECONNECT_SESSION_KEY } from "../../constant"; import { useGrazInternalStore, useGrazSessionStore } from "../../store"; -import type { Wallet } from "../../types/wallet"; +import type { SignAminoParams, SignDirectParams, Wallet } from "../../types/wallet"; import { WalletType } from "../../types/wallet"; export const getCapsule = (): Wallet => { @@ -30,7 +32,7 @@ export const getCapsule = (): Wallet => { useGrazInternalStore.setState({ capsuleState: { showModal: true, chainId } }); }; - const onSuccessLogin = async () => { + const onAfterLoginSuccessful = async () => { const client = useGrazSessionStore.getState().capsuleClient; const { chains } = useGrazInternalStore.getState(); if (!client) throw new Error("Capsule client is not initialized"); @@ -103,12 +105,63 @@ export const getCapsule = (): Wallet => { return client.getOfflineSigner(chainId); }; + const getOfflineSignerAmino = (chainId: string) => { + const client = useGrazSessionStore.getState().capsuleClient; + if (!client) throw new Error("Capsule client is not initialized"); + return client.getOfflineSignerAmino(chainId); + }; + + const getOfflineSignerDirect = (chainId: string) => { + const client = useGrazSessionStore.getState().capsuleClient; + if (!client) throw new Error("Capsule client is not initialized"); + return client.getOfflineSignerDirect(chainId); + }; + + // eslint-disable-next-line @typescript-eslint/require-await + const getOfflineSignerAuto = async (chainId: string) => { + const client = useGrazSessionStore.getState().capsuleClient; + if (!client) throw new Error("Capsule client is not initialized"); + return client.getOfflineSignerDirect(chainId); + }; + + const signDirect = async (...args: SignDirectParams): Promise => { + const [chainId, signer, signDoc] = args; + const client = useGrazSessionStore.getState().capsuleClient; + if (!client) throw new Error("Capsule client is not initialized"); + return client.signDirect(chainId, signer, { + bodyBytes: signDoc.bodyBytes!, + authInfoBytes: signDoc.authInfoBytes!, + chainId: signDoc.chainId!, + accountNumber: signDoc.accountNumber!, + }) as Promise; + }; + + const signAmino = async (...args: SignAminoParams): Promise => { + const [chainId, signer, signDoc, signOptions] = args; + const client = useGrazSessionStore.getState().capsuleClient; + if (!client) throw new Error("Capsule client is not initialized"); + return client.signAmino(chainId, signer, signDoc, signOptions) as Promise; + }; + + const experimentalSuggestChain = async (..._args: Parameters) => { + await Promise.reject(new Error("Capsule does not support experimentalSuggestChain")); + }; + return { init, enable, - onSuccessLogin, + onAfterLoginSuccessful, getKey, + getOfflineSignerAuto, + getOfflineSignerDirect, + signDirect, + signAmino, + experimentalSuggestChain, + setDefaultOptions: () => { + console.log("setDefaultOptions not supported by capsule"); + }, // @ts-expect-error - CapsuleAminoSigner | OfflineDirectSigner getOfflineSigner, + getOfflineSignerAmino, }; }; diff --git a/packages/graz/src/hooks/account.ts b/packages/graz/src/hooks/account.ts index 7a6c75b8..8e2d6ea2 100644 --- a/packages/graz/src/hooks/account.ts +++ b/packages/graz/src/hooks/account.ts @@ -273,13 +273,7 @@ export type UseConnectChainArgs = MutationEventArgs; export const useConnect = ({ onError, onLoading, onSuccess }: UseConnectChainArgs = {}) => { const queryKey = ["USE_CONNECT", onError, onLoading, onSuccess]; const mutation = useMutation(queryKey, connect, { - onError: (err, args) => - Promise.resolve(() => { - // @ts-expect-error - ignore - if (err?.message !== "CAPSULE_OPEN_MODAL") { - onError?.(err, args); - } - }), + onError: (err, args) => onError?.(err, args), onMutate: onLoading, onSuccess: (connectResult) => Promise.resolve(onSuccess?.(connectResult)), }); diff --git a/packages/graz/src/hooks/capsule.ts b/packages/graz/src/hooks/capsule.ts index 243bdd40..6a47f3fc 100644 --- a/packages/graz/src/hooks/capsule.ts +++ b/packages/graz/src/hooks/capsule.ts @@ -1,3 +1,4 @@ +import { disconnect } from "../actions/account"; import { getCapsule } from "../actions/wallet/capsule"; import { useGrazInternalStore, useGrazSessionStore } from "../store"; @@ -7,14 +8,19 @@ export const useCapsule = () => { const capsule = getCapsule(); return { - setModalState: (state: boolean) => - useGrazInternalStore.setState({ + setModalState: (state: boolean) => { + useGrazInternalStore.setState((prev) => ({ capsuleState: { showModal: state, + chainId: prev.capsuleState?.chainId, }, - }), + })); + }, modalState: Boolean(capsuleState?.showModal), client: capsuleClient, - onSuccessfulLogin: capsule.onSuccessLogin, + onAfterLoginSuccessful: capsule.onAfterLoginSuccessful, + onLoginFailure: () => { + void disconnect(); + }, }; }; diff --git a/packages/graz/src/hooks/signingClients.ts b/packages/graz/src/hooks/signingClients.ts index a8c6b43c..43e84ff7 100644 --- a/packages/graz/src/hooks/signingClients.ts +++ b/packages/graz/src/hooks/signingClients.ts @@ -64,7 +64,6 @@ export function useStargateSigningClient( () => ["USE_STARGATE_SIGNING_CLIENT", chains, wallet, args, activeChainIds] as const, [activeChainIds, args, chains, wallet], ); - return useQuery({ queryKey, queryFn: async ({ queryKey: [, _chains, _wallet] }) => { diff --git a/packages/graz/src/store/index.ts b/packages/graz/src/store/index.ts index 5004b12a..f093e2ee 100644 --- a/packages/graz/src/store/index.ts +++ b/packages/graz/src/store/index.ts @@ -1,5 +1,5 @@ import type { ChainInfo, Keplr, Key } from "@keplr-wallet/types"; -import type { CapsuleEnvironment, CapsuleProvider } from "@leapwallet/cosmos-social-login-capsule-provider"; +import type { CapsuleProvider } from "@leapwallet/cosmos-social-login-capsule-provider"; import type { ISignClient, SignClientTypes } from "@walletconnect/types"; import type { Web3ModalConfig } from "@web3modal/standalone"; import { create } from "zustand"; @@ -25,7 +25,7 @@ export interface WalletConnectStore { export interface CapsuleConfig { apiKey: string; - env: CapsuleEnvironment; + env: "DEV" | "SANDBOX" | "BETA" | "PROD"; } export interface CapsuleState { @@ -64,7 +64,7 @@ export type GrazSessionPersistedStore = Pick; export const grazInternalDefaultValues: GrazInternalStore = { @@ -110,6 +110,7 @@ const persistOptions: PersistOptions Promise; disable?: (chainIds?: string | undefined) => Promise; setDefaultOptions?: (options: KeplrIntereactionOptions) => void; - onSuccessLogin?: () => Promise; + onAfterLoginSuccessful?: () => Promise; }; export type SignDirectParams = Parameters; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b57beda..4072675c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -273,14 +273,14 @@ importers: specifier: 0.0.6 version: 0.0.6 long: - specifier: '*' + specifier: ^4 version: 4.0.0 wadesta: specifier: ^0.0.5 version: 0.0.5(long@4.0.0) zustand: - specifier: ^4.4.1 - version: 4.4.1(@types/react@18.2.21)(react@18.2.0) + specifier: ^4.5.2 + version: 4.5.2(@types/react@18.2.21)(react@18.2.0) devDependencies: '@types/node': specifier: ^18.17.15 @@ -9801,7 +9801,7 @@ packages: eventemitter3: 4.0.7 typescript: 5.2.2 viem: 1.5.3(typescript@5.2.2)(zod@3.20.6) - zustand: 4.4.1(@types/react@18.2.21)(react@18.2.0) + zustand: 4.5.2(@types/react@18.2.21)(react@18.2.0) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -23453,12 +23453,12 @@ packages: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} dev: false - /zustand@4.4.1(@types/react@18.2.21)(react@18.2.0): - resolution: {integrity: sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==} + /zustand@4.5.2(@types/react@18.2.21)(react@18.2.0): + resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==} engines: {node: '>=12.7.0'} peerDependencies: '@types/react': '>=16.8' - immer: '>=9.0' + immer: '>=9.0.6' react: '>=16.8' peerDependenciesMeta: '@types/react':