UNPKG

moonchute

Version:

React hook for account abstraction

877 lines (855 loc) 20.8 kB
"use client"; import { encodeFunctionData, isAddress, toBytes } from "./chunk-R6I7NKL2.js"; import "./chunk-PWB7F2CX.js"; // src/hooks/useCreateSmartAccount.ts import * as React3 from "react"; import { useAccount, useNetwork } from "wagmi"; // src/actions/getUnsignedUserOperation.ts import { getPublicClient } from "@wagmi/core"; import axios from "axios"; async function getUnsignedUserOperation({ account, owner, chainId, appId, abi, functionName, args, address, value }) { const publicClient = getPublicClient({ chainId }); let calldata = "0x"; if (abi && functionName) { await publicClient.simulateContract({ abi, address, functionName, args, account: account === "0x" ? void 0 : account }); calldata = encodeFunctionData({ abi, args, functionName }); } const unsigned_options = { method: "POST", url: "https://api.moonchute.xyz/userop", headers: { accept: "application/json", "content-type": "application/json", "x-app-id": appId }, data: { jsonrpc: "2.0", id: 1, method: "mc_getUnsignedUserOp", params: [ { account, owner, chainId, to: address, value: value?.toString() || "0", data: calldata } ] } }; const res = await axios.request(unsigned_options); if (res.status !== 200) { throw new Error(`Failed to get unsigned user operation: ${res.data}`); } return res.data; } // src/actions/sendUserOperation.ts import { getWalletClient } from "@wagmi/core"; import axios2 from "axios"; async function sendUserOperation({ userOp, userOpHash, chainId, accountType, appId }) { const walletClient = await getWalletClient(); if (!walletClient) throw new Error("Wallet client not found"); const signature = await walletClient.signMessage({ message: { raw: toBytes(userOpHash) } }); const unsigned_options = { method: "POST", url: "https://api.moonchute.xyz/userop", headers: { accept: "application/json", "content-type": "application/json", "x-app-id": appId }, data: { jsonrpc: "2.0", id: 1, method: "mc_sendUserOp", params: [ { userOp, chainId, signature, accountType } ] } }; const res = await axios2.request(unsigned_options); if (res.status !== 200) { throw new Error(`Failed to send user operation: ${res.data}`); } return { userOpHash: res.data }; } // src/actions/createSmartAccount.ts async function createSmartAccount(args) { const { owner, chainId, address, value, abi, functionName, args: functionArgs, appId } = args; const unsignedRes = await getUnsignedUserOperation({ owner, chainId, address, value, abi, functionName, args: functionArgs, appId }); const { userOp, userOpHash, accountType } = unsignedRes; return await sendUserOperation({ userOp, userOpHash, chainId, accountType, appId }); } // src/context.ts import { QueryClientProvider } from "@tanstack/react-query"; import * as React from "react"; var Context = React.createContext(void 0); var queryClientContext = React.createContext( void 0 ); function MoonChuteConfig({ children, config }) { return React.createElement(Context.Provider, { value: config, children: React.createElement(QueryClientProvider, { children, client: config.queryClient, context: queryClientContext }) }); } function useConfig() { const config = React.useContext(Context); if (!config) { throw new Error("useConfig must be used within a MoonChuteConfig"); } return config; } // src/hooks/query/useMutation.ts import { parseMutationArgs, useMutation as useMutation_ } from "@tanstack/react-query"; function useMutation(arg1, arg2, arg3) { const options = parseMutationArgs(arg1, arg2, arg3); return useMutation_({ context: queryClientContext, ...options }); } // src/hooks/query/useQuery.ts import { QueryObserver } from "@tanstack/react-query"; // src/hooks/query/useBaseQuery.ts import { notifyManager, useIsRestoring, useQueryClient, useQueryErrorResetBoundary } from "@tanstack/react-query"; import * as React2 from "react"; // src/hooks/query/utils.ts function isQueryKey(value) { return Array.isArray(value); } function isPlainObject(o) { if (!hasObjectPrototype(o)) { return false; } const ctor = o.constructor; if (typeof ctor === "undefined") { return true; } const prot = ctor.prototype; if (!hasObjectPrototype(prot)) { return false; } if (!prot.hasOwnProperty("isPrototypeOf")) { return false; } return true; } function hasObjectPrototype(o) { return Object.prototype.toString.call(o) === "[object Object]"; } function parseQueryArgs(arg1, arg2, arg3) { if (!isQueryKey(arg1)) { return arg1; } if (typeof arg2 === "function") { return { ...arg3, queryKey: arg1, queryFn: arg2 }; } return { ...arg2, queryKey: arg1 }; } function queryKeyHashFn(queryKey3) { return JSON.stringify( queryKey3, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => { result[key] = val[key]; return result; }, {}) : typeof val === "bigint" ? val.toString() : val ); } function shouldThrowError(_useErrorBoundary, params) { if (typeof _useErrorBoundary === "function") { return _useErrorBoundary(...params); } return !!_useErrorBoundary; } function trackResult(result, observer) { const trackedResult = {}; Object.keys(result).forEach((key) => { Object.defineProperty(trackedResult, key, { configurable: false, enumerable: true, get: () => { observer.trackedProps.add(key); return result[key]; } }); }); return trackedResult; } // src/hooks/query/useSyncExternalStore.ts import * as pkg from "use-sync-external-store/shim/index.js"; var useSyncExternalStore2 = pkg.useSyncExternalStore; // src/hooks/query/useBaseQuery.ts function useBaseQuery(options, Observer) { const queryClient = useQueryClient({ context: options.context }); const isRestoring = useIsRestoring(); const errorResetBoundary = useQueryErrorResetBoundary(); const defaultedOptions = queryClient.defaultQueryOptions({ ...options, queryKeyHashFn }); defaultedOptions._optimisticResults = isRestoring ? "isRestoring" : "optimistic"; if (defaultedOptions.onError) { defaultedOptions.onError = notifyManager.batchCalls( defaultedOptions.onError ); } if (defaultedOptions.onSuccess) { defaultedOptions.onSuccess = notifyManager.batchCalls( defaultedOptions.onSuccess ); } if (defaultedOptions.onSettled) { defaultedOptions.onSettled = notifyManager.batchCalls( defaultedOptions.onSettled ); } if (defaultedOptions.suspense) { if (typeof defaultedOptions.staleTime !== "number") { defaultedOptions.staleTime = 1e3; } } if (defaultedOptions.suspense || defaultedOptions.useErrorBoundary) { if (!errorResetBoundary.isReset()) { defaultedOptions.retryOnMount = false; } } const [observer] = React2.useState( () => new Observer( queryClient, defaultedOptions ) ); const result = observer.getOptimisticResult(defaultedOptions); useSyncExternalStore2( React2.useCallback( (onStoreChange) => isRestoring ? () => void 0 : observer.subscribe(notifyManager.batchCalls(onStoreChange)), [observer, isRestoring] ), () => observer.getCurrentResult(), () => observer.getCurrentResult() ); React2.useEffect(() => { errorResetBoundary.clearReset(); }, [errorResetBoundary]); React2.useEffect(() => { observer.setOptions(defaultedOptions, { listeners: false }); }, [defaultedOptions, observer]); if (defaultedOptions.suspense && result.isLoading && result.isFetching && !isRestoring) { throw observer.fetchOptimistic(defaultedOptions).then(({ data }) => { defaultedOptions.onSuccess?.(data); defaultedOptions.onSettled?.(data, null); }).catch((error) => { errorResetBoundary.clearReset(); defaultedOptions.onError?.(error); defaultedOptions.onSettled?.(void 0, error); }); } if (result.isError && !errorResetBoundary.isReset() && !result.isFetching && shouldThrowError(defaultedOptions.useErrorBoundary, [ result.error, observer.getCurrentQuery() ])) { throw result.error; } const status = result.status === "loading" && result.fetchStatus === "idle" ? "idle" : result.status; const isIdle = status === "idle"; const isLoading = status === "loading" && result.fetchStatus === "fetching"; return { ...result, defaultedOptions, isIdle, isLoading, observer, status }; } // src/hooks/query/useQuery.ts function useQuery(arg1, arg2, arg3) { const parsedOptions = parseQueryArgs(arg1, arg2, arg3); const baseQuery = useBaseQuery({ context: queryClientContext, ...parsedOptions }, QueryObserver); const result = { data: baseQuery.data, error: baseQuery.error, fetchStatus: baseQuery.fetchStatus, isError: baseQuery.isError, isFetched: baseQuery.isFetched, isFetchedAfterMount: baseQuery.isFetchedAfterMount, isFetching: baseQuery.isFetching, isIdle: baseQuery.isIdle, isLoading: baseQuery.isLoading, isRefetching: baseQuery.isRefetching, isSuccess: baseQuery.isSuccess, refetch: baseQuery.refetch, status: baseQuery.status, internal: { dataUpdatedAt: baseQuery.dataUpdatedAt, errorUpdatedAt: baseQuery.errorUpdatedAt, failureCount: baseQuery.failureCount, // TODO: Remove `isFetchedAfterMount` in next minor version (v0.8). isFetchedAfterMount: baseQuery.isFetchedAfterMount, isLoadingError: baseQuery.isLoadingError, isPaused: baseQuery.isPaused, isPlaceholderData: baseQuery.isPlaceholderData, isPreviousData: baseQuery.isPreviousData, isRefetchError: baseQuery.isRefetchError, isStale: baseQuery.isStale, remove: baseQuery.remove } }; return !baseQuery.defaultedOptions.notifyOnChangeProps ? trackResult(result, baseQuery.observer) : result; } // src/hooks/useCreateSmartAccount.ts function mutationKey({ ...config }) { const { owner, chainId, appId, address, value, abi, functionName, args } = config; return [ { entity: "createSmartAccount", owner, chainId, appId, address, value, abi, functionName, args } ]; } function mutationFn(config) { if (!config) { throw new Error("Config is required"); } const { owner, chainId, appId, address, value, abi, functionName, args } = config; if (!owner) { throw new Error("Owner is required"); } if (!chainId) { throw new Error("ChainId is required"); } if (!appId) { throw new Error("APP ID is required"); } if (address) { if (abi) { if (!functionName) { throw new Error("functionName is required"); } } if (functionName) { if (!abi) { throw new Error("abi is required"); } } } return createSmartAccount({ owner, chainId, address, value, abi, functionName, args, appId }); } function useCreateSmartAccount(config) { const moonchuteConfig = useConfig(); const appId = moonchuteConfig.store.getState().appId; const { chain } = useNetwork(); const { address: ownerAddress } = useAccount(); const { data, error, isError, isIdle, isLoading, isSuccess, mutate, mutateAsync, reset, status, variables } = useMutation( mutationKey({ ...config, appId, owner: ownerAddress, chainId: chain?.id }), mutationFn, {} ); const write = React3.useMemo(() => { if (!config) return void 0; const { address, value, abi, functionName, args } = config; if (!ownerAddress || !chain || !appId) return void 0; if (address) { if (!abi && !value) return void 0; } return () => mutate({ owner: ownerAddress, chainId: chain?.id, appId, address, value, abi, functionName, args }); }, [ config, mutate, appId, ownerAddress, chain ]); return { data, error, isError, isIdle, isLoading, isSuccess, mutate, mutateAsync, reset, status, variables, write }; } // src/hooks/usePrepareUserOperation.ts import { useAccount as useAccount2, useNetwork as useNetwork2 } from "wagmi"; function queryKey({ account, owner, chainId, address, functionName, args, value }) { return [ { entity: "prepareUserOperation", account, owner, chainId, address, functionName, args, value } ]; } function queryFn({ abi, appId }) { return ({ queryKey: [{ account, owner, address, chainId, value, functionName, args }] }) => { if (!account && (!owner || !isAddress(owner))) { throw new Error("owner is required for creating account"); } if (account && !isAddress(account)) { throw new Error("account is not a valid address"); } if (!address) { throw new Error("address is required"); } if (abi) { if (!functionName) { throw new Error("functionName is required"); } } if (functionName) { if (!abi) { throw new Error("abi is required"); } } if (!functionName && !abi && !value) { throw new Error("functionName or value is required"); } if (!appId) { throw new Error("appId is required"); } if (!chainId) { throw new Error("chainId is required"); } return getUnsignedUserOperation({ account, owner, chainId, appId, address, value, abi, functionName, args }); }; } function usePrepareUserOperation({ account, address, value, abi, functionName, args }) { const config = useConfig(); const { chain } = useNetwork2(); const { address: ownerAddress } = useAccount2(); const appId = config.appId; const prepareUserOperationQuery = useQuery( queryKey({ account, owner: ownerAddress, chainId: chain?.id, address, value, functionName, args }), queryFn({ abi, appId }), {} ); return Object.assign(prepareUserOperationQuery, { config: { ...prepareUserOperationQuery.data } }); } // src/hooks/useSendUserOperation.ts import * as React4 from "react"; function mutationKey2({ ...config }) { const { userOp, userOpHash, chainId, accountType, appId } = config; return [ { entity: "sendUserOperation", userOp, userOpHash, chainId, accountType, appId } ]; } function mutationFn2(config) { if (!config) { throw new Error("Config is required"); } const { userOp, userOpHash, chainId, accountType, appId } = config; if (!chainId) { throw new Error("ChainId is required"); } if (!accountType) { throw new Error("Account type is required"); } if (!userOp) { throw new Error("User operation is required"); } if (!userOpHash) { throw new Error("User operation hash is required"); } if (!appId) { throw new Error("API key is required"); } return sendUserOperation({ userOp, userOpHash, chainId, accountType, appId }); } function useSendUserOperation(config) { const moonchuteConfig = useConfig(); const appId = moonchuteConfig.store.getState().appId; const { data, error, isError, isIdle, isLoading, isSuccess, mutate, mutateAsync, reset, status, variables } = useMutation( mutationKey2({ ...config, appId }), mutationFn2, {} ); const write = React4.useMemo(() => { if (!config) return void 0; const { userOp, userOpHash, chainId, accountType } = config; if (!userOp || !userOpHash || !chainId || !accountType || !appId) return void 0; return () => mutate({ userOp, userOpHash, chainId, accountType, appId }); }, [config, mutate, appId]); return { data, error, isError, isIdle, isLoading, isSuccess, reset, status, variables, write }; } // src/hooks/useSmartAccounts.ts import * as React5 from "react"; // src/actions/fetchSmartAccounts.ts import axios3 from "axios"; async function fetchSmartAccounts({ address, chainId, appId }) { let smartAccount = void 0; try { if (!address || !chainId) { throw new Error("Address and chainId are required"); } if (!appId) { throw new Error("API key is required"); } const { data } = await axios3.get( `https://api.moonchute.xyz/account?address=${address}&chainId=${chainId}`, { headers: { "x-app-id": appId } } ); smartAccount = data.smartAccount; } catch (err) { throw err; } return { smartAccount }; } // src/hooks/useSmartAccounts.ts function queryKey2({ address, chainId, appId }) { return [{ entity: "smartaccounts", address, chainId, appId }]; } function queryFn2({ queryKey: [{ address, chainId, appId }] }) { return fetchSmartAccounts({ address, chainId, appId }); } function useSmartAccounts({ address, chainId }) { const config = useConfig(); const appId = config.appId; const queryKey_ = React5.useMemo( () => queryKey2({ address, chainId, appId }), [address, chainId, appId] ); const smartAccountsQuery = useQuery(queryKey_, queryFn2, {}); return smartAccountsQuery; } // src/config.ts import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"; import { QueryClient } from "@tanstack/react-query"; import { persistQueryClient } from "@tanstack/react-query-persist-client"; import { persist, subscribeWithSelector } from "zustand/middleware"; import { createStore } from "zustand/vanilla"; // src/storage.ts var noopStorage = { getItem: (_key) => "", setItem: (_key, _value) => null, removeItem: (_key) => null }; function createStorage({ prefix = "chute", storage }) { return { ...storage, getItem: (key, defaultState = null) => { const value = storage.getItem(`${prefix}.${key}`); return value === null ? defaultState : JSON.parse(value); }, setItem: (key, value) => { if (value === null) { storage.removeItem(`${prefix}.${key}`); } else { storage.setItem(`${prefix}.${key}`, JSON.stringify(value)); } }, removeItem: (key) => storage.removeItem(`${prefix}.${key}`) }; } // src/config.ts var storeKey = "store"; var Config = class { constructor({ storage = createStorage({ storage: typeof window !== "undefined" ? window.localStorage : noopStorage }), appId }) { this.storage = storage; this.storage.setItem("appId", appId); let status = "success"; this.store = createStore( subscribeWithSelector( persist( // eslint-disable-next-line @typescript-eslint/consistent-type-assertions () => ({ status }), { name: storeKey, storage, partialize: (state) => ({ status: state.status }), version: 2 } ) ) ); this.store.setState({ appId }); } get appId() { return this.store.getState().appId; } }; function createMoonChuteConfig({ queryClient = new QueryClient({ defaultOptions: { queries: { cacheTime: 1e3 * 60 * 60 * 24, // 24 hours networkMode: "offlineFirst", refetchOnWindowFocus: false, retry: 0 }, mutations: { networkMode: "offlineFirst" } } }), storage = createStorage({ storage: typeof window !== "undefined" && window.localStorage ? window.localStorage : noopStorage }), persister = typeof window !== "undefined" ? createSyncStoragePersister({ key: "cache", serialize: (x) => x, deserialize: (x) => x, storage }) : void 0, ...args }) { const config = new Config({ ...args, storage }); if (persister) { persistQueryClient({ queryClient, persister, dehydrateOptions: { shouldDehydrateQuery: (query) => query.cacheTime !== 0 && query.queryKey[0].persist !== false } }); } return Object.assign(config, { queryClient }); } export { MoonChuteConfig, createMoonChuteConfig, useCreateSmartAccount, usePrepareUserOperation, useSendUserOperation, useSmartAccounts };