moonchute
Version:
React hook for account abstraction
877 lines (855 loc) • 20.8 kB
JavaScript
"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
};