diff --git a/biome.json b/biome.json index a249232..22a2406 100644 --- a/biome.json +++ b/biome.json @@ -23,7 +23,8 @@ "rules": { "recommended": true, "suspicious": { - "noExplicitAny": "warn" + "noExplicitAny": "warn", + "noConfusingVoidType": "warn" }, "style": { "noUnusedTemplateLiteral": "warn", diff --git a/bun.lockb b/bun.lockb index fb9d983..667449d 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 2a7fd16..d430603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@zerodev/waas", - "version": "0.1.10", + "version": "0.2.0", "description": "", "main": "dist/index.cjs", "module": "dist/index.js", @@ -23,12 +23,13 @@ "@zerodev/ecdsa-validator": "^5.2.3", "@zerodev/passkey-validator": "^5.2.3", "@zerodev/permissions": "^5.2.2", - "@zerodev/sdk": "^5.2.4", + "@zerodev/sdk": "^5.2.10", "@zerodev/session-key": "^5.2.2", "@zerodev/social-validator": "5.0.1", "events": "^3.3.0", "lodash": "^4.17.21", - "pino-pretty": "^11.0.0" + "pino-pretty": "^11.0.0", + "zustand": "4.4.1" }, "devDependencies": { "@biomejs/biome": "^1.7.1", diff --git a/src/actions/createBasicSession.ts b/src/actions/createBasicSession.ts new file mode 100644 index 0000000..29bb8fe --- /dev/null +++ b/src/actions/createBasicSession.ts @@ -0,0 +1,82 @@ +import type { Evaluate } from "@wagmi/core/internal" +import type { KernelValidator } from "@zerodev/sdk" +import type { Permission } from "@zerodev/session-key" +import { ENTRYPOINT_ADDRESS_V06 } from "permissionless" +import type { EntryPoint } from "permissionless/types" +import { http, type Abi, createPublicClient } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import type { Config } from "../createConfig" +import { + KernelClientNotConnectedError, + type KernelClientNotConnectedErrorType, + KernelClientNotSupportedError, + type KernelClientNotSupportedErrorType, + PermissionsEmptyError, + type PermissionsEmptyErrorType, + ZerodevNotConfiguredError, + type ZerodevNotConfiguredErrorType +} from "../errors" +import { ZERODEV_BUNDLER_URL } from "../utils/constants" +import { createSessionKernelAccount, createSessionKey } from "../utils/sessions" + +export type CreateBasicSessionParameters = Evaluate<{ + permissions: Permission[] +}> + +export type CreateBasicSessionReturnType = Evaluate<{ + chainId: number + sessionKey: `0x${string}` + sessionId: `0x${string}` + smartAccount: `0x${string}` + enableSignature: `0x${string}` + permissions: Permission[] +}> + +export type CreateBasicSessionErrorType = + | ZerodevNotConfiguredErrorType + | KernelClientNotSupportedErrorType + | PermissionsEmptyErrorType + | KernelClientNotConnectedErrorType + +export async function createBasicSession( + entryPoint: TEntryPoint | null, + validator: KernelValidator | null, + config: Config, + parameters: CreateBasicSessionParameters +): Promise { + const { permissions } = parameters + + const chainId = config.state.chainId + const selectedChain = config.chains.find((x) => x.id === chainId) + if (!selectedChain) { + throw new ZerodevNotConfiguredError() + } + const publicClient = config.getClient({ chainId }) + + if (!entryPoint || !validator) throw new KernelClientNotConnectedError() + + if (entryPoint !== ENTRYPOINT_ADDRESS_V06) { + throw new KernelClientNotSupportedError("create basicSession", "v3") + } + if (!permissions || permissions.length === 0) + throw new PermissionsEmptyError() + + const sessionKey = createSessionKey() + const sessionSigner = privateKeyToAccount(sessionKey) + + const kernelAccount = await createSessionKernelAccount({ + sessionSigner, + publicClient: publicClient, + sudoValidator: validator, + entryPoint: entryPoint, + permissions: permissions + }) + return { + chainId, + sessionKey: sessionKey, + sessionId: kernelAccount.sessionId, + smartAccount: kernelAccount.smartAccount, + enableSignature: kernelAccount.enableSignature, + permissions: kernelAccount.permissions + } +} diff --git a/src/actions/createKernelClientEOA.ts b/src/actions/createKernelClientEOA.ts new file mode 100644 index 0000000..c6e9f0d --- /dev/null +++ b/src/actions/createKernelClientEOA.ts @@ -0,0 +1,103 @@ +import { connect, getAccount, getWalletClient, switchChain } from "@wagmi/core" +import type { Evaluate } from "@wagmi/core/internal" +import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator" +import { + type KernelSmartAccount, + type KernelValidator, + createKernelAccount +} from "@zerodev/sdk" +import { walletClientToSmartAccountSigner } from "permissionless" +import type { EntryPoint } from "permissionless/types" +import { http, createPublicClient } from "viem" +import type { + ResourceUnavailableRpcErrorType, + UserRejectedRequestErrorType +} from "viem" +import type { Config, Connector, CreateConnectorFn } from "wagmi" +import type { Config as ZdConfig } from "../createConfig" +import { + ZerodevNotConfiguredError, + type ZerodevNotConfiguredErrorType +} from "../errors" +import type { KernelVersionType } from "../types" +import { ZERODEV_BUNDLER_URL } from "../utils/constants" +import { getEntryPointFromVersion } from "../utils/entryPoint" + +export type CreateKernelClientEOAParameters = Evaluate<{ + connector: Connector | CreateConnectorFn +}> + +export type CreateKernelClientEOAReturnType = { + validator: KernelValidator + kernelAccount: KernelSmartAccount + entryPoint: EntryPoint +} + +export type CreateKernelClientEOAErrorType = + | ZerodevNotConfiguredErrorType + | ResourceUnavailableRpcErrorType + | UserRejectedRequestErrorType + +export async function createKernelClientEOA( + wagmiConfig: Config, + zdConfig: ZdConfig, + version: KernelVersionType, + parameters: CreateKernelClientEOAParameters +) { + const { connector } = parameters + + const chainId = zdConfig.state.chainId + const chain = zdConfig.chains.find((x) => x.id === chainId) + if (!chain) throw new ZerodevNotConfiguredError() + + const client = zdConfig.getClient({ chainId }) + + const entryPoint = getEntryPointFromVersion(version) + + const { status } = getAccount(wagmiConfig) + + const isConnected = + "uid" in connector && connector.uid === wagmiConfig.state.current + + if (status === "disconnected" && !isConnected) { + await connect(wagmiConfig, { connector, chainId: chainId }) + } else { + if (wagmiConfig.state.chainId !== chainId) { + await switchChain(wagmiConfig, { chainId }) + } + } + const walletClient = await getWalletClient(wagmiConfig) + + const ecdsaValidator = await signerToEcdsaValidator(client, { + entryPoint: entryPoint, + signer: walletClientToSmartAccountSigner(walletClient) + }) + const account = await createKernelAccount(client, { + entryPoint: entryPoint, + plugins: { + sudo: ecdsaValidator + } + }) + const uid = `ecdsa:${account.address}` + + zdConfig.setState((x) => { + const chainId = x.chainId + return { + ...x, + connections: new Map(x.connections).set(uid, { + chainId, + accounts: [ + { + client: null, + account: account, + entryPoint, + validator: ecdsaValidator + } + ] + }), + current: uid + } + }) + + return { validator: ecdsaValidator, kernelAccount: account, entryPoint } +} diff --git a/src/actions/createKernelClientPasskey.ts b/src/actions/createKernelClientPasskey.ts new file mode 100644 index 0000000..42972b4 --- /dev/null +++ b/src/actions/createKernelClientPasskey.ts @@ -0,0 +1,106 @@ +import type { Evaluate } from "@wagmi/core/internal" +import { + createPasskeyValidator, + getPasskeyValidator +} from "@zerodev/passkey-validator" +import { + type KernelSmartAccount, + type KernelValidator, + createKernelAccount +} from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { http, createPublicClient } from "viem" +import type { Config } from "../createConfig" +import { + PasskeyRegisterNoUsernameError, + type PasskeyRegisterNoUsernameErrorType, + ZerodevNotConfiguredError, + type ZerodevNotConfiguredErrorType +} from "../errors" +import type { KernelVersionType } from "../types" +import { ZERODEV_PASSKEY_URL } from "../utils/constants" +import { ZERODEV_BUNDLER_URL } from "../utils/constants" +import { getEntryPointFromVersion } from "../utils/entryPoint" +import { getWeb3AuthNValidatorFromVersion } from "../utils/webauthn" + +export type PasskeConnectType = "register" | "login" + +export type CreateKernelClientPasskeyParameters = Evaluate<{ + type: PasskeConnectType + username?: string | undefined +}> + +export type CreateKernelClientPasskeyReturnType = { + validator: KernelValidator + kernelAccount: KernelSmartAccount + entryPoint: EntryPoint +} + +export type CreateKernelClientPasskeyErrorType = + | ZerodevNotConfiguredErrorType + | PasskeyRegisterNoUsernameErrorType + +export async function createKernelClientPasskey( + config: Config, + version: KernelVersionType, + parameters: CreateKernelClientPasskeyParameters +) { + const { type, username } = parameters + + const chainId = config.state.chainId + const chain = config.chains.find((x) => x.id === chainId) + if (!chain) throw new ZerodevNotConfiguredError() + const projectId = config.projectIds[chainId] + const client = config.getClient({ chainId }) + + let passkeyValidator: KernelValidator + const entryPoint = getEntryPointFromVersion(version) + const webauthnValidator = getWeb3AuthNValidatorFromVersion(entryPoint) + + if (type === "register") { + if (!username) { + throw new PasskeyRegisterNoUsernameError() + } + passkeyValidator = await createPasskeyValidator(client, { + passkeyName: username, + passkeyServerUrl: `${ZERODEV_PASSKEY_URL}/${projectId}`, + entryPoint: entryPoint, + validatorAddress: webauthnValidator + }) + } else { + passkeyValidator = await getPasskeyValidator(client, { + passkeyServerUrl: `${ZERODEV_PASSKEY_URL}/${projectId}`, + entryPoint: entryPoint, + validatorAddress: webauthnValidator + }) + } + + const kernelAccount = await createKernelAccount(client, { + entryPoint: entryPoint, + plugins: { + sudo: passkeyValidator + } + }) + const uid = `passkey:${kernelAccount.address}` + + config.setState((x) => { + const chainId = x.chainId + return { + ...x, + connections: new Map(x.connections).set(uid, { + chainId, + accounts: [ + { + client: null, + account: kernelAccount, + entryPoint, + validator: passkeyValidator + } + ] + }), + current: uid + } + }) + + return { validator: passkeyValidator, kernelAccount, entryPoint } +} diff --git a/src/actions/createSession.ts b/src/actions/createSession.ts new file mode 100644 index 0000000..f2adfb4 --- /dev/null +++ b/src/actions/createSession.ts @@ -0,0 +1,83 @@ +import type { Evaluate } from "@wagmi/core/internal" +import type { Policy } from "@zerodev/permissions" +import type { KernelValidator } from "@zerodev/sdk" +import { ENTRYPOINT_ADDRESS_V07 } from "permissionless" +import type { EntryPoint } from "permissionless/types" +import { http, createPublicClient } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import type { Config } from "../createConfig" +import { + KernelClientNotConnectedError, + type KernelClientNotConnectedErrorType, + KernelClientNotSupportedError, + type KernelClientNotSupportedErrorType, + PoliciesEmptyError, + type PoliciesEmptyErrorType, + ZerodevNotConfiguredError, + type ZerodevNotConfiguredErrorType +} from "../errors" +import { ZERODEV_BUNDLER_URL } from "../utils/constants" +import { createSessionKernelAccount, createSessionKey } from "../utils/sessions" + +export type CreateSessionParameters = Evaluate<{ + policies: Policy[] +}> + +export type CreateSessionReturnType = Evaluate<{ + sessionKey: `0x${string}` + sessionId: `0x${string}` + smartAccount: `0x${string}` + enableSignature: `0x${string}` + chainId: number + policies: Policy[] +}> + +export type CreateSessionErrorType = + | ZerodevNotConfiguredErrorType + | KernelClientNotSupportedErrorType + | KernelClientNotConnectedErrorType + | PoliciesEmptyErrorType + +export async function createSession( + entryPoint: TEntryPoint | null, + validator: KernelValidator | null, + config: Config, + parameters: CreateSessionParameters +): Promise { + const { policies } = parameters + + const chainId = config.state.chainId + const selectedChain = config.chains.find((x) => x.id === chainId) + if (!selectedChain) { + throw new ZerodevNotConfiguredError() + } + const publicClient = config.getClient({ chainId }) + + if (!entryPoint || !validator) throw new KernelClientNotConnectedError() + + if (entryPoint !== ENTRYPOINT_ADDRESS_V07) { + throw new KernelClientNotSupportedError("create session", "v2") + } + + if (!policies || policies.length === 0) throw new PoliciesEmptyError() + + const sessionKey = createSessionKey() + const sessionSigner = privateKeyToAccount(sessionKey) + + const kernelAccount = await createSessionKernelAccount({ + sessionSigner, + publicClient, + sudoValidator: validator, + entryPoint: entryPoint, + policies: policies + }) + + return { + sessionKey: sessionKey, + sessionId: kernelAccount.sessionId, + chainId: chainId, + smartAccount: kernelAccount.smartAccount, + enableSignature: kernelAccount.enableSignature, + policies: kernelAccount.policies + } +} diff --git a/src/actions/disconnectKernelClient.ts b/src/actions/disconnectKernelClient.ts new file mode 100644 index 0000000..4e1a434 --- /dev/null +++ b/src/actions/disconnectKernelClient.ts @@ -0,0 +1,16 @@ +import type { BaseError } from "@wagmi/core" + +export type DisconnectKernelClientParameters = unknown + +export type DisconnectKernelClientReturnType = void + +export type DisconnectKernelClientErrorType = typeof BaseError + +export async function disconnectKernelClient( + disconnectKernelClient: () => void, + logoutSocial: () => Promise +): Promise { + await logoutSocial() + disconnectKernelClient() + return +} diff --git a/src/actions/getBalance.ts b/src/actions/getBalance.ts new file mode 100644 index 0000000..efb99e3 --- /dev/null +++ b/src/actions/getBalance.ts @@ -0,0 +1,131 @@ +import { + type Address, + ContractFunctionExecutionError, + type ContractFunctionExecutionErrorType, + type Hex, + type PublicClient, + formatUnits, + hexToString, + trim +} from "viem" +import { + KernelClientNotConnectedError, + type KernelClientNotConnectedErrorType +} from "../errors" + +export type GetBalanceParameters = { + symbolType: "string" | "bytes32" + address: Address + tokenAddress: Address +} + +export type GetBalanceReturnType = { + value: bigint + decimals: number + symbol: string + formatted: string +} + +export type GetBalanceErrorType = + | KernelClientNotConnectedErrorType + | ContractFunctionExecutionErrorType + +export async function getBalance( + publicClient: PublicClient, + parameters: GetBalanceParameters +): Promise { + const { address, tokenAddress } = parameters + const chain = publicClient.chain + + if (!chain) { + throw new KernelClientNotConnectedError() + } + if (tokenAddress) { + try { + return await getTokenBalance(publicClient, { + address: address, + symbolType: "string", + tokenAddress + }) + } catch (error) { + // In the chance that there is an error upon decoding the contract result, + // it could be likely that the contract data is represented as bytes32 instead + // of a string. + if (error instanceof ContractFunctionExecutionError) { + const balance = await getTokenBalance(publicClient, { + address: address, + symbolType: "bytes32", + tokenAddress + }) + const symbol = hexToString( + trim(balance.symbol as Hex, { dir: "right" }) + ) + return { ...balance, symbol } + } + throw error + } + } + const balance = await publicClient.getBalance({ + address: address + }) + + return { + value: balance, + decimals: chain.nativeCurrency.decimals, + symbol: chain.nativeCurrency.symbol, + formatted: formatUnits(balance, chain.nativeCurrency.decimals) + } +} + +async function getTokenBalance( + publicClient: PublicClient, + parameters: GetBalanceParameters +): Promise { + const { tokenAddress, address, symbolType } = parameters + const contract = { + abi: [ + { + type: "function", + name: "balanceOf", + stateMutability: "view", + inputs: [{ type: "address" }], + outputs: [{ type: "uint256" }] + }, + { + type: "function", + name: "decimals", + stateMutability: "view", + inputs: [], + outputs: [{ type: "uint8" }] + }, + { + type: "function", + name: "symbol", + stateMutability: "view", + inputs: [], + outputs: [{ type: symbolType }] + } + ], + address: tokenAddress + } + + const [value, decimals, symbol] = await Promise.all( + [ + { + ...contract, + functionName: "balanceOf", + args: [address] + }, + { ...contract, functionName: "decimals" }, + { ...contract, functionName: "symbol" } + ].map((contract) => publicClient.readContract(contract)) + ) + const formatted = formatUnits(value as bigint, decimals as number) + + return { + decimals: decimals as number, + formatted, + symbol: symbol as string, + value: value as bigint + } +} diff --git a/src/actions/getChainId.ts b/src/actions/getChainId.ts new file mode 100644 index 0000000..59a680c --- /dev/null +++ b/src/actions/getChainId.ts @@ -0,0 +1,10 @@ +import type { Config } from "../createConfig" + +export type GetChainIdReturnType = + TConfig["chains"][number]["id"] + +export function getChainId( + config: TConfig +): GetChainIdReturnType { + return config.state.chainId +} diff --git a/src/actions/getChains.ts b/src/actions/getChains.ts new file mode 100644 index 0000000..0b5eff0 --- /dev/null +++ b/src/actions/getChains.ts @@ -0,0 +1,19 @@ +import type { Chain } from "viem" +import type { Config } from "../createConfig" +import { deepEqual } from "../utils/deepEqual" + +export type GetChainsReturnType = + | TConfig["chains"] + | readonly [Chain, ...Chain[]] + +let previousChains: readonly Chain[] = [] + +export function getChains( + config: TConfig +): GetChainsReturnType { + const chains = config.chains + if (deepEqual(previousChains, chains)) + return previousChains as GetChainsReturnType + previousChains = chains + return chains +} diff --git a/src/actions/getKernelClient.ts b/src/actions/getKernelClient.ts new file mode 100644 index 0000000..0d56d1b --- /dev/null +++ b/src/actions/getKernelClient.ts @@ -0,0 +1,127 @@ +import { + type KernelAccountClient, + type KernelSmartAccount, + createKernelAccountClient, + createZeroDevPaymasterClient, + gasTokenAddresses +} from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { http, type Address, type Chain, type Transport } from "viem" +import type { Config } from "../createConfig" +import { + ERC20PaymasterTokenNotSupportedError, + type ERC20PaymasterTokenNotSupportedErrorType, + type KernelClientNotConnectedErrorType, + ZerodevNotConfiguredError +} from "../errors" +import type { + GasTokenChainIdType, + GasTokenType, + PaymasterERC20, + PaymasterSPONSOR +} from "../types" +import { ZERODEV_BUNDLER_URL, ZERODEV_PAYMASTER_URL } from "../utils/constants" + +export type GetKernelClientParameters = { + paymaster?: PaymasterERC20 | PaymasterSPONSOR +} + +export type GetKernelClientReturnType = { + address: Address | undefined + entryPoint: EntryPoint | undefined + kernelAccount: KernelSmartAccount | undefined + kernelClient: KernelAccountClient | undefined + isConnected: boolean +} + +export type GetKernelClientErrorType = + | KernelClientNotConnectedErrorType + | ERC20PaymasterTokenNotSupportedErrorType + +export async function getKernelClient( + config: Config, + kernelAccountClient: KernelAccountClient | null, + kernelAccount: KernelSmartAccount | null, + parameters: GetKernelClientParameters +): Promise { + const { paymaster } = parameters + + if (kernelAccountClient?.account) { + return { + kernelClient: kernelAccountClient, + kernelAccount: kernelAccountClient.account, + address: kernelAccountClient.account.address, + entryPoint: kernelAccountClient.account.entryPoint, + isConnected: true + } + } + if (!kernelAccount) { + return { + kernelClient: undefined, + kernelAccount: undefined, + address: undefined, + entryPoint: undefined, + isConnected: false + } + } + + const chainId = config.state.chainId + const selectedChain = config.chains.find((x) => x.id === chainId) + if (!selectedChain) { + throw new ZerodevNotConfiguredError() + } + const projectId = config.projectIds[selectedChain.id] + + const kernelClient = createKernelAccountClient({ + account: kernelAccount, + chain: selectedChain, + bundlerTransport: http(`${ZERODEV_BUNDLER_URL}/${projectId}`), + entryPoint: kernelAccount.entryPoint, + middleware: !paymaster + ? undefined + : { + sponsorUserOperation: async ({ userOperation }) => { + let gasToken: GasTokenType | undefined + if (paymaster.type === "ERC20") { + const chainId = config.state + .chainId as GasTokenChainIdType + if ( + !(chainId in gasTokenAddresses) || + !( + paymaster.gasToken in + gasTokenAddresses[chainId] + ) + ) { + throw new ERC20PaymasterTokenNotSupportedError( + paymaster.gasToken, + chainId + ) + } + gasToken = + paymaster.gasToken as keyof (typeof gasTokenAddresses)[typeof chainId] + } + + const kernelPaymaster = createZeroDevPaymasterClient({ + entryPoint: kernelAccount.entryPoint, + chain: selectedChain, + transport: http( + `${ZERODEV_PAYMASTER_URL}/${projectId}?paymasterProvider=PIMLICO` + ) + }) + return kernelPaymaster.sponsorUserOperation({ + userOperation, + entryPoint: kernelAccount.entryPoint, + gasToken: gasToken + }) + } + } + }) + + return { + kernelClient, + kernelAccount: kernelClient.account, + address: kernelClient.account.address, + entryPoint: kernelClient.account.entryPoint, + isConnected: true + } +} diff --git a/src/actions/getSessionKernelClient.ts b/src/actions/getSessionKernelClient.ts new file mode 100644 index 0000000..4c13646 --- /dev/null +++ b/src/actions/getSessionKernelClient.ts @@ -0,0 +1,144 @@ +import { + ERC20PaymasterTokenNotSupportedError, + type ERC20PaymasterTokenNotSupportedErrorType, + type KernelClientNotConnectedErrorType, + SessionIdMissingError, + type SessionIdMissingErrorType, + SessionNotAvailableError, + SessionNotFoundError, + type SessionNotFoundErrorType, + ZerodevNotConfiguredError, + type ZerodevNotConfiguredErrorType +} from "../errors" + +import { + type KernelAccountClient, + type KernelSmartAccount, + type KernelValidator, + createKernelAccountClient, + createZeroDevPaymasterClient, + gasTokenAddresses +} from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { http, type Address, type Chain, createPublicClient } from "viem" +import { privateKeyToAccount } from "viem/accounts" +import type { Config } from "../createConfig" +import type { + GasTokenChainIdType, + GasTokenType, + PaymasterERC20, + PaymasterSPONSOR, + SessionType +} from "../types" +import { ZERODEV_BUNDLER_URL, ZERODEV_PAYMASTER_URL } from "../utils/constants" +import { getSessionKernelAccount } from "../utils/sessions/" + +export type GetSessionKernelClientParameters = { + sessionId?: `0x${string}` | null | undefined + paymaster?: PaymasterERC20 | PaymasterSPONSOR +} + +export type GetSessionKernelClientReturnType = { + kernelClient: KernelAccountClient | undefined + kernelAccount: KernelSmartAccount | undefined +} + +export type GetSessionKernelClientErrorType = + | KernelClientNotConnectedErrorType + | SessionNotFoundErrorType + | SessionIdMissingErrorType + | ERC20PaymasterTokenNotSupportedErrorType + | ZerodevNotConfiguredErrorType + +export async function getSessionKernelClient( + config: Config, + validator: KernelValidator, + kernelAddress: Address, + entryPoint: EntryPoint, + session: SessionType | null, + parameters: GetSessionKernelClientParameters +): Promise { + const { sessionId, paymaster } = parameters + + const chainId = config.state.chainId + const selectedChain = config.chains.find((x) => x.id === chainId) + if (!selectedChain) { + throw new ZerodevNotConfiguredError() + } + const projectId = config.projectIds[selectedChain.id] + if (!session) { + throw new SessionNotFoundError() + } + const accountSession = Object.values(session).filter( + (s) => s.smartAccount === kernelAddress + ) + if (accountSession.length === 0) { + throw new SessionNotAvailableError(kernelAddress) + } + if (accountSession.length > 1 && !sessionId) { + throw new SessionIdMissingError() + } + const selectedSession = sessionId ? session[sessionId] : accountSession[0] + + const sessionSigner = privateKeyToAccount(selectedSession.sessionKey) + const client = config.getClient({ chainId }) + const { kernelAccount } = await getSessionKernelAccount({ + sessionSigner, + publicClient: client, + sudoValidator: validator, + entryPoint: entryPoint, + policies: selectedSession.policies, + permissions: selectedSession.permissions, + enableSignature: selectedSession.enableSignature + }) + + const kernelClient = createKernelAccountClient({ + account: kernelAccount, + chain: selectedChain, + bundlerTransport: http(`${ZERODEV_BUNDLER_URL}/${projectId}`), + entryPoint: entryPoint, + middleware: !paymaster + ? undefined + : { + sponsorUserOperation: async ({ userOperation }) => { + let gasToken: GasTokenType | undefined + if (paymaster.type === "ERC20") { + const chainId = + selectedChain.id as GasTokenChainIdType + if ( + !(chainId in gasTokenAddresses) || + !( + paymaster.gasToken in + gasTokenAddresses[chainId] + ) + ) { + throw new ERC20PaymasterTokenNotSupportedError( + paymaster.gasToken, + chainId + ) + } + gasToken = + paymaster.gasToken as keyof (typeof gasTokenAddresses)[typeof chainId] + } + + const kernelPaymaster = createZeroDevPaymasterClient({ + entryPoint: entryPoint, + chain: selectedChain, + transport: http( + `${ZERODEV_PAYMASTER_URL}/${projectId}?paymasterProvider=PIMLICO` + ) + }) + return kernelPaymaster.sponsorUserOperation({ + userOperation, + entryPoint: entryPoint, + gasToken: gasToken + }) + } + } + }) + + return { + kernelClient, + kernelAccount + } +} diff --git a/src/actions/sendTransaction.ts b/src/actions/sendTransaction.ts new file mode 100644 index 0000000..9109a71 --- /dev/null +++ b/src/actions/sendTransaction.ts @@ -0,0 +1,50 @@ +import type { SendTransactionParameters as viem_sendTransactionParameters } from "@wagmi/core" +import { + type KernelAccountClient, + getCustomNonceKeyFromString +} from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import type { Hash } from "viem" +import { + KernelClientNotConnectedError, + type KernelClientNotConnectedErrorType +} from "../errors" +import { generateRandomString } from "../utils" + +export type SendTransactionParameters = viem_sendTransactionParameters[] + +export type SendTransactionReturnType = Hash + +export type SendTransactionErrorType = KernelClientNotConnectedErrorType + +export async function sendTransaction( + kernelClient: KernelAccountClient | undefined | null, + isParallel: boolean, + nonceKey: string | undefined, + parameters: SendTransactionParameters +): Promise { + if (!kernelClient || !kernelClient.account) { + throw new KernelClientNotConnectedError() + } + const kernelAccount = kernelClient.account + let nonce: bigint | undefined + if (nonceKey || isParallel) { + const seedForNonce = nonceKey ? nonceKey : generateRandomString() + const customNonceKey = getCustomNonceKeyFromString( + seedForNonce, + kernelAccount.entryPoint + ) + nonce = await kernelAccount.getNonce(customNonceKey) + } + + const txHash = await kernelClient.sendTransactions({ + transactions: parameters.map((p) => ({ + ...p, + data: p.data ?? "0x", + value: p.value ?? 0n + })), + nonce + }) + + return txHash +} diff --git a/src/actions/sendUserOperation.ts b/src/actions/sendUserOperation.ts new file mode 100644 index 0000000..178f99b --- /dev/null +++ b/src/actions/sendUserOperation.ts @@ -0,0 +1,55 @@ +import type { WriteContractParameters } from "@wagmi/core" +import { + type KernelAccountClient, + getCustomNonceKeyFromString +} from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { type Hash, encodeFunctionData } from "viem" +import { + KernelClientNotConnectedError, + type KernelClientNotConnectedErrorType +} from "../errors" +import { generateRandomString } from "../utils" + +export type SendUserOperationParameters = WriteContractParameters[] + +export type SendUserOperationReturnType = Hash + +export type SendUserOperationErrorType = KernelClientNotConnectedErrorType + +export async function sendUserOperation( + kernelClient: KernelAccountClient | undefined | null, + isParallel: boolean, + nonceKey: string | undefined, + parameters: SendUserOperationParameters +): Promise { + if (!kernelClient || !kernelClient.account) { + throw new KernelClientNotConnectedError() + } + const kernelAccount = kernelClient.account + + let nonce: bigint | undefined + if (nonceKey || isParallel) { + const seedForNonce = nonceKey ? nonceKey : generateRandomString() + const customNonceKey = getCustomNonceKeyFromString( + seedForNonce, + kernelAccount.entryPoint + ) + nonce = await kernelAccount.getNonce(customNonceKey) + } + + const userOpHash = await kernelClient.sendUserOperation({ + userOperation: { + callData: await kernelAccount.encodeCallData( + parameters.map((p) => ({ + to: p.address, + value: p.value ?? 0n, + data: encodeFunctionData(p) + })) + ), + nonce + } + }) + + return userOpHash +} diff --git a/src/actions/setKernelClient.ts b/src/actions/setKernelClient.ts new file mode 100644 index 0000000..03ac9ed --- /dev/null +++ b/src/actions/setKernelClient.ts @@ -0,0 +1,25 @@ +import type { KernelAccountClient } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { + KernelClientInvalidError, + type KernelClientInvalidErrorType +} from "../errors" + +export type SetKernelClientParameters = KernelAccountClient + +export type SetKernelClientReturnType = boolean + +export type SetKernelClientErrorType = KernelClientInvalidErrorType + +export async function setKernelClient( + setKernelAccountClient: ( + kernelAccountClient: KernelAccountClient + ) => void, + parameters: SetKernelClientParameters +) { + if (!parameters) throw new KernelClientInvalidError() + + setKernelAccountClient(parameters) + + return true +} diff --git a/src/actions/switchChain.ts b/src/actions/switchChain.ts new file mode 100644 index 0000000..2db238d --- /dev/null +++ b/src/actions/switchChain.ts @@ -0,0 +1,157 @@ +import { getWalletClient, switchChain as wagmi_switchChain } from "@wagmi/core" +import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator" +import { + deserializePasskeyValidator, + getPasskeyValidator +} from "@zerodev/passkey-validator" +import { + type KernelSmartAccount, + type KernelValidator, + createKernelAccount +} from "@zerodev/sdk" +import { getSocialValidator } from "@zerodev/social-validator" +import { walletClientToSmartAccountSigner } from "permissionless" +import type { EntryPoint } from "permissionless/types" +import type { Config as WagmiConfig } from "wagmi" +import type { Config as ZdConfig } from "../createConfig" +import { + KernelAlreadyOnTheChainError, + KernelClientNotConnectedError, + ZerodevNotConfiguredError +} from "../errors" +import { ZERODEV_PASSKEY_URL } from "../utils/constants" +import { getWeb3AuthNValidatorFromVersion } from "../utils/webauthn" + +export type SwitchChainParameters< + TZdConfig extends ZdConfig = ZdConfig, + TChainId extends + TZdConfig["chains"][number]["id"] = TZdConfig["chains"][number]["id"] +> = { + chainId: TChainId | ZdConfig["chains"][number]["id"] +} + +export type SwitchChainReturnType< + TConfig extends ZdConfig = ZdConfig, + TChainId extends + TConfig["chains"][number]["id"] = TConfig["chains"][number]["id"] +> = { + id: number + kernelAccount: KernelSmartAccount | null + kernelValidator: KernelValidator | null +} + +export type SwitchChainErrorType = + | ZerodevNotConfiguredError + | KernelClientNotConnectedError + +export async function switchChain< + TZdConfig extends ZdConfig, + TChainId extends TZdConfig["chains"][number]["id"] +>( + zdConfig: TZdConfig, + wagmiConfig: WagmiConfig, + kernelValidator: KernelValidator | null, + parameters: SwitchChainParameters +): Promise> { + const { chainId } = parameters + + const chain = zdConfig.chains.find((x) => x.id === chainId) + const uid = zdConfig.state.current + + if (!chain) { + throw new ZerodevNotConfiguredError() + } + if (chainId === zdConfig.state.chainId) { + throw new KernelAlreadyOnTheChainError() + } + if (!uid) { + throw new KernelClientNotConnectedError() + } + const projectId = zdConfig.projectIds[chainId] + const entryPoint = + zdConfig.state.connections.get(uid)?.accounts[0].entryPoint + if (!entryPoint) { + throw new KernelClientNotConnectedError() + } + const client = zdConfig.getClient({ chainId }) + + const type = uid.split(":")[0] + let kernelAccount: KernelSmartAccount | null = null + let validator: KernelValidator | null = null + + if (type === "ecdsa") { + if (wagmiConfig.state.chainId !== chainId) { + await wagmi_switchChain(wagmiConfig, { chainId }) + } + + // reconstruct kernel client + const walletClient = await getWalletClient(wagmiConfig) + const ecdsaValidator = await signerToEcdsaValidator(client, { + entryPoint: entryPoint, + signer: walletClientToSmartAccountSigner(walletClient) + }) + validator = ecdsaValidator + kernelAccount = await createKernelAccount(client, { + entryPoint: entryPoint, + plugins: { + sudo: ecdsaValidator + } + }) + } else if (type === "passkey") { + // reconstruct kernel client + let passkeyValidator: KernelValidator + + // use serialized data if passkey validator exists + if (kernelValidator) { + const serializedData = ( + kernelValidator as KernelValidator< + EntryPoint, + "WebAuthnValidator" + > & { + getSerializedData: () => string + } + ).getSerializedData() + + passkeyValidator = await deserializePasskeyValidator(client, { + serializedData, + entryPoint + }) + validator = passkeyValidator + } else { + const webauthnValidator = + getWeb3AuthNValidatorFromVersion(entryPoint) + passkeyValidator = await getPasskeyValidator(client, { + passkeyServerUrl: `${ZERODEV_PASSKEY_URL}/${projectId}`, + entryPoint: entryPoint, + validatorAddress: webauthnValidator + }) + validator = passkeyValidator + } + kernelAccount = await createKernelAccount(client, { + entryPoint: entryPoint, + plugins: { + sudo: passkeyValidator + } + }) + } else if (type === "social") { + validator = await getSocialValidator(client, { + entryPoint, + projectId + }) + kernelAccount = await createKernelAccount(client, { + entryPoint: entryPoint, + plugins: { + sudo: validator + } + }) + } + + zdConfig.setState((x) => ({ ...x, chainId })) + + return { + // chain as SwitchChainReturnType + id: chainId, + kernelValidator: validator, + kernelAccount + } +} diff --git a/src/actions/watchChainId.ts b/src/actions/watchChainId.ts new file mode 100644 index 0000000..8b75617 --- /dev/null +++ b/src/actions/watchChainId.ts @@ -0,0 +1,19 @@ +import type { Config } from "../createConfig" +import type { GetChainIdReturnType } from "./getChainId" + +export type WatchChainIdParameters = { + onChange( + chainId: GetChainIdReturnType, + prevChainId: GetChainIdReturnType + ): void +} + +export type WatchChainIdReturnType = () => void + +export function watchChainId( + config: TConfig, + parameters: WatchChainIdParameters +): WatchChainIdReturnType { + const { onChange } = parameters + return config.subscribe((state) => state.chainId, onChange) +} diff --git a/src/actions/watchChains.ts b/src/actions/watchChains.ts new file mode 100644 index 0000000..7d4f9ae --- /dev/null +++ b/src/actions/watchChains.ts @@ -0,0 +1,26 @@ +import type { Config } from "../createConfig" +import type { GetChainsReturnType } from "./getChains" + +export type WatchChainsParameters = { + onChange( + chains: GetChainsReturnType, + prevChains: GetChainsReturnType + ): void +} + +export type WatchChainsReturnType = () => void + +/** + * @internal + * We don't expose this because as far as consumers know, you can't chainge (lol) `config.chains` at runtime. + * Setting `config.chains` via `config._internal.chains.setState(...)` is an extremely advanced use case that's not worth documenting or supporting in the public API at this time. + */ +export function watchChains( + config: TConfig, + parameters: WatchChainsParameters +): WatchChainsReturnType { + const { onChange } = parameters + return config._internal.chains.subscribe((chains, prevChains) => { + onChange(chains, prevChains) + }) +} diff --git a/src/createConfig.ts b/src/createConfig.ts new file mode 100644 index 0000000..7c82770 --- /dev/null +++ b/src/createConfig.ts @@ -0,0 +1,298 @@ +import type { Evaluate } from "@wagmi/core/internal" +import type { ExactPartial } from "@wagmi/core/internal" +import type { + KernelAccountClient, + KernelSmartAccount, + KernelValidator +} from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import type { Chain, Client, PublicClient, Transport } from "viem" +import { createPublicClient } from "viem" +import { persist, subscribeWithSelector } from "zustand/middleware" +import { type Mutate, type StoreApi, createStore } from "zustand/vanilla" +import { + KernelClientNotConnectedError, + ZerodevNotConfiguredError +} from "./errors" +import { version } from "./utils/config" +import { + type Storage, + createStorage, + noopStorage +} from "./utils/config/createStorage" + +export type CreateConfigParameters< + TChains extends readonly [Chain, ...Chain[]] = readonly [Chain, ...Chain[]], + TProjectIds extends Record = Record< + TChains[number]["id"], + string + >, + TTransports extends Record = Record< + TChains[number]["id"], + Transport + > +> = Evaluate<{ + chains: TChains + projectIds: TProjectIds + transports: TTransports + storage?: Storage | null | undefined + ssr?: boolean | undefined +}> + +export function createConfig< + const TChains extends readonly [Chain, ...Chain[]], + TProjectIds extends Record, + TTransports extends Record = Record< + TChains[number]["id"], + Transport + > +>( + parameters: CreateConfigParameters +): Config { + const { + storage = createStorage({ + key: "zerodev", + storage: + typeof window !== "undefined" && window.localStorage + ? window.localStorage + : noopStorage + }), + ssr, + ...rest + } = parameters + + const chains = createStore(() => rest.chains) + const projectIds = createStore(() => rest.projectIds) + const transports = createStore(() => rest.transports) + + const clients = new Map>() + function getClient( + config: { chainId?: TChainId | TChains[number]["id"] | undefined } = {} + ): PublicClient { + const chainId = config.chainId ?? store.getState().chainId + const chain = chains.getState().find((x) => x.id === chainId) + + // chainId specified and not configured + if (config.chainId && !chain) throw new ZerodevNotConfiguredError() + + // If the target chain is not configured, use the client of the current chain. + type Return = PublicClient + { + const client = clients.get(store.getState().chainId) + if (client && !chain) return client as Return + else if (!chain) throw new ZerodevNotConfiguredError() + } + + // If a memoized client exists for a chain id, use that. + { + const client = clients.get(chainId) + if (client) return client as Return + } + + const client = createPublicClient({ + chain, + transport: rest.transports[chainId as TChainId] + }) + + clients.set(chainId, client) + return client as Return + } + + function getInitialState() { + return { + chainId: chains.getState()[0].id, + current: null, + connections: new Map() + } satisfies State + } + + let currentVersion: number + const prefix = "0.0.0-canary-" + if (version.startsWith(prefix)) + currentVersion = Number.parseInt(version.replace(prefix, "")) + else currentVersion = Number.parseInt(version.split(".")[0] ?? "0") + + const store = createStore( + subscribeWithSelector( + // only use persist middleware if storage exists + storage + ? persist(getInitialState, { + migrate(persistedState, version) { + if (version === currentVersion) + return persistedState as State + + const initialState = getInitialState() + const chainId = + persistedState && + typeof persistedState === "object" && + "chainId" in persistedState && + typeof persistedState.chainId === "number" + ? persistedState.chainId + : initialState.chainId + return { ...initialState, chainId } + }, + name: "store", + partialize(state) { + // Only persist "critical" store properties to preserve storage size. + return { + chainId: state.chainId, + current: state.current, + connections: state.connections + } satisfies PartializedState + }, + skipHydration: ssr, + storage: storage as Storage>, + version: currentVersion + }) + : getInitialState + ) + ) + + return { + get chains() { + return chains.getState() as TChains + }, + get projectIds() { + return projectIds.getState() as TProjectIds + }, + get transports() { + return transports.getState() as TTransports + }, + storage, + + getClient, + get state() { + return store.getState() as unknown as State + }, + setState(value) { + let newState: State + if (typeof value === "function") + newState = value(store.getState() as any) + else newState = value + + // Reset state if it got set to something not matching the base state + const initialState = getInitialState() + if (typeof newState !== "object") newState = initialState + const isCorrupt = Object.keys(initialState).some( + (x) => !(x in newState) + ) + if (isCorrupt) newState = initialState + + store.setState(newState, true) + }, + subscribe(selector, listener, options) { + return store.subscribe( + selector as unknown as (state: State) => any, + listener, + options + ? { ...options, fireImmediately: options.emitImmediately } + : undefined + ) + }, + _internal: { + store, + ssr: Boolean(ssr), + chains: { + setState(value) { + const nextChains = ( + typeof value === "function" + ? value(chains.getState()) + : value + ) as TChains + if (nextChains.length === 0) return + return chains.setState(nextChains, true) + }, + subscribe(listener) { + return chains.subscribe(listener) + } + } + } + } +} + +export type Config< + TChains extends readonly [Chain, ...Chain[]] = readonly [Chain, ...Chain[]], + TProjectIds extends Record = Record< + TChains[number]["id"], + string + >, + TTransports extends Record = Record< + TChains[number]["id"], + Transport + > +> = { + readonly chains: TChains + readonly projectIds: TProjectIds + readonly transports: TTransports + readonly storage: Storage | null + + readonly state: State + setState( + value: State | ((state: State) => State) + ): void + subscribe( + selector: (state: State) => state, + listener: (state: state, previousState: state) => void, + options?: + | { + emitImmediately?: boolean | undefined + equalityFn?: ((a: state, b: state) => boolean) | undefined + } + | undefined + ): () => void + + getClient(parameters?: { + chainId?: chainId | TChains[number]["id"] | undefined + }): PublicClient + + _internal: { + readonly store: Mutate, [["zustand/persist", any]]> + readonly ssr: boolean + + chains: { + setState( + value: + | readonly [Chain, ...Chain[]] + | (( + state: readonly [Chain, ...Chain[]] + ) => readonly [Chain, ...Chain[]]) + ): void + subscribe( + listener: ( + state: readonly [Chain, ...Chain[]], + prevState: readonly [Chain, ...Chain[]] + ) => void + ): () => void + } + } +} + +export type PaymasterType = "SPONSOR" | "ERC20" | "NO" +export type CurrentClient = { + uid: string + paymaster: PaymasterType +} + +export type State< + TChains extends readonly [Chain, ...Chain[]] = readonly [Chain, ...Chain[]] +> = { + chainId: TChains[number]["id"] + current: string | null + connections: Map +} + +export type KernelClient = { + client: KernelAccountClient | null + account: KernelSmartAccount + entryPoint: EntryPoint + validator: KernelValidator +} + +export type Connection = { + accounts: readonly [KernelClient, ...KernelClient[]] + chainId: number +} + +export type PartializedState = Evaluate< + ExactPartial> +> diff --git a/src/errors/config.ts b/src/errors/config.ts new file mode 100644 index 0000000..df326d7 --- /dev/null +++ b/src/errors/config.ts @@ -0,0 +1,12 @@ +import { BaseError } from "@wagmi/core" + +export type ZerodevNotConfiguredErrorType = ZerodevNotConfiguredError & { + name: "ZerodevNotConfiguredError" +} + +export class ZerodevNotConfiguredError extends BaseError { + override name = "ZerodevNotConfiguredError" + constructor() { + super("ZeroDev not configured.") + } +} diff --git a/src/errors/index.ts b/src/errors/index.ts new file mode 100644 index 0000000..22b07fb --- /dev/null +++ b/src/errors/index.ts @@ -0,0 +1,40 @@ +export { + type ZerodevNotConfiguredErrorType, + ZerodevNotConfiguredError +} from "./config" + +export { + type PasskeyRegisterNoUsernameErrorType, + PasskeyRegisterNoUsernameError +} from "./passkey" + +export { + type SocialNoAuthorizedErrorType, + SocialNoAuthorizedError +} from "./social" + +export { + type KernelClientInvalidErrorType, + type KernelClientNotSupportedErrorType, + type KernelClientNotConnectedErrorType, + type ERC20PaymasterTokenNotSupportedErrorType, + type KernelAlreadyOnTheChainErrorType, + KernelClientInvalidError, + KernelClientNotSupportedError, + KernelClientNotConnectedError, + ERC20PaymasterTokenNotSupportedError, + KernelAlreadyOnTheChainError +} from "./kernel" + +export { + type PermissionsEmptyErrorType, + type PoliciesEmptyErrorType, + type SessionNotFoundErrorType, + type SessionNotAvailableErrorType, + type SessionIdMissingErrorType, + PermissionsEmptyError, + PoliciesEmptyError, + SessionNotFoundError, + SessionNotAvailableError, + SessionIdMissingError +} from "./session" diff --git a/src/errors/kernel.ts b/src/errors/kernel.ts new file mode 100644 index 0000000..1029322 --- /dev/null +++ b/src/errors/kernel.ts @@ -0,0 +1,60 @@ +import { BaseError } from "@wagmi/core" +import { Address } from "viem" + +export type KernelClientInvalidErrorType = KernelClientInvalidError & { + name: "KernelClientInvalidErrorType" +} + +export class KernelClientInvalidError extends BaseError { + override name = "KernelClientInvalidErrorType" + constructor() { + super("KernelClient is invalid.") + } +} + +export type KernelClientNotSupportedErrorType = + KernelClientNotSupportedError & { + name: "KernelClientNotSupportedError" + } + +export class KernelClientNotSupportedError extends BaseError { + override name = "KernelClientNotSupportedError" + constructor(action: string, version: string) { + super(`KernelClient: ${action} is not supported in ${version}.`) + } +} + +export type KernelClientNotConnectedErrorType = + KernelClientNotConnectedError & { + name: "KernelClientNotConnectedErrorType" + } + +export class KernelClientNotConnectedError extends BaseError { + override name = "KernelClientNotConnectedError" + constructor() { + super("KernelClient not connected.") + } +} + +export type KernelAlreadyOnTheChainErrorType = KernelAlreadyOnTheChainError & { + name: "KernelAlreadyOnTheChainErrorType" +} + +export class KernelAlreadyOnTheChainError extends BaseError { + override name = "KernelAlreadyOnTheChainError" + constructor() { + super("KernelClient already on the chain.") + } +} + +export type ERC20PaymasterTokenNotSupportedErrorType = + ERC20PaymasterTokenNotSupportedError & { + name: "ERC20PaymasterTokenNotSupportedErrorType" + } + +export class ERC20PaymasterTokenNotSupportedError extends BaseError { + override name = "ERC20PaymasterTokenNotSupportedError" + constructor(token: string, chain: number) { + super(`ERC20 ${token} not supported on chain ${chain}.`) + } +} diff --git a/src/errors/passkey.ts b/src/errors/passkey.ts new file mode 100644 index 0000000..07b28f5 --- /dev/null +++ b/src/errors/passkey.ts @@ -0,0 +1,13 @@ +import { BaseError } from "@wagmi/core" + +export type PasskeyRegisterNoUsernameErrorType = + PasskeyRegisterNoUsernameError & { + name: "PasskeyRegisterNoUsernameError" + } + +export class PasskeyRegisterNoUsernameError extends BaseError { + override name = "PasskeyRegisterNoUsernameError" + constructor() { + super("Username is required to register passkey.") + } +} diff --git a/src/errors/session.ts b/src/errors/session.ts new file mode 100644 index 0000000..b56b720 --- /dev/null +++ b/src/errors/session.ts @@ -0,0 +1,56 @@ +import { BaseError } from "@wagmi/core" + +export type PermissionsEmptyErrorType = PermissionsEmptyError & { + name: "PermissionsInvalidErrorType" +} + +export class PermissionsEmptyError extends BaseError { + override name = "PermissionsEmptyError" + constructor() { + super("Permission can not be empty.") + } +} + +export type PoliciesEmptyErrorType = PoliciesEmptyError & { + name: "PoliciesEmptyErrorType" +} + +export class PoliciesEmptyError extends BaseError { + override name = "PoliciesEmptyError" + constructor() { + super("Policies can not be empty.") + } +} + +export type SessionNotFoundErrorType = SessionNotFoundError & { + name: "SessionNotFoundErrorType" +} + +export class SessionNotFoundError extends BaseError { + override name = "SessionNotFoundError" + constructor() { + super("Session not found.") + } +} + +export type SessionNotAvailableErrorType = SessionNotAvailableError & { + name: "SessionNotAvailableErrorType" +} + +export class SessionNotAvailableError extends BaseError { + override name = "SessionNotAvailableError" + constructor(account: `0x${string}`) { + super(`No available session for ${account}.`) + } +} + +export type SessionIdMissingErrorType = SessionIdMissingError & { + name: "SessionIdMissingErrorType" +} + +export class SessionIdMissingError extends BaseError { + override name = "SessionIdMissingError" + constructor() { + super("Session id is required.") + } +} diff --git a/src/errors/social.ts b/src/errors/social.ts new file mode 100644 index 0000000..f5e01db --- /dev/null +++ b/src/errors/social.ts @@ -0,0 +1,12 @@ +import { BaseError } from "@wagmi/core" + +export type SocialNoAuthorizedErrorType = SocialNoAuthorizedError & { + name: "SocialNoAuthorizedError" +} + +export class SocialNoAuthorizedError extends BaseError { + override name = "SocialNoAuthorizedError" + constructor() { + super("Socail login projectId is not authorized.") + } +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts index 5ea3b65..2f05d90 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -6,70 +6,60 @@ export { export { useCreateBasicSession, - type UseCreateBasicSessionReturnType, - type CreateBasicSessionVariables, - type CreateBasicSessionReturnType + type UseCreateBasicSessionReturnType } from "./useCreateBasicSession" export { useCreateSession, - type UseCreateSessionReturnType, - type CreateSessionVariables, - type CreateSessionReturnType + type UseCreateSessionReturnType } from "./useCreateSession" export { useCreateKernelClientEOA, type UseCreateKernelClientEOAParameters, - type UseCreateKernelClientEOAReturnType, - type CreateKernelClientEOAVariables, - type CreateKernelClientEOAReturnType + type UseCreateKernelClientEOAReturnType } from "./useCreateKernelClientEOA" export { useCreateKernelClientPasskey, type UseCreateKernelClientPasskeyParameters, - type UseCreateKernelClientPasskeyReturnType, - type CreateKernelClientPasskeyVariables, - type CreateKernelClientPasskeyReturnType + type UseCreateKernelClientPasskeyReturnType } from "./useCreateKernelClientPasskey" export { useKernelClient, type UseKernelClientParameters, - type UseKernelClientReturnType, - type GetKernelClientReturnType + type UseKernelClientReturnType } from "./useKernelClient" export { useSendUserOperation, type UseSendUserOperationParameters, - type UseSendUserOperationReturnType, - type SendUserOperationVariables, - type SendUserOperationReturnType + type UseSendUserOperationReturnType } from "./useSendUserOperation" export { useSendUserOperationWithSession, type UseSendUserOperationWithSessionParameters, - type UseSendUserOperationWithSessionReturnType, - type SendUserOperationWithSessionVariables, - type SendUserOperationWithSessionReturnType + type UseSendUserOperationWithSessionReturnType } from "./useSendUserOperationWithSession" export { useSendTransaction, type UseSendTransactionParameters, - type UseSendTransactionReturnType, - type SendTransactionVariables, - type SendTransactionReturnType + type UseSendTransactionReturnType } from "./useSendTransaction" +export { + useSendTransactionWithSession, + type UseSendTransactionWithSessionParameters, + type UseSendTransactionWithSessionReturnType +} from "./useSendTransactionWithSession" + export { useSessionKernelClient, type UseSessionKernelClientParameters, - type UseSessionKernelClientReturnType, - type GetSessionKernelClientReturnType + type UseSessionKernelClientReturnType } from "./useSessionKernelClient" export { @@ -79,26 +69,43 @@ export { export { useSetKernelClient, - type UseSetKernelClientReturnType, - type SetKernelClientReturnType + type UseSetKernelClientReturnType } from "./useSetKernelClient" export { useDisconnectKernelClient, - type UseDisconnectKernelClientReturnType, - type DisconnectKernelClientReturnType + type UseDisconnectKernelClientReturnType } from "./useDisconnectKernelClient" export { useBalance, type UseBalanceParameters, - type UseBalanceReturnType, - type GetBalanceReturnType + type UseBalanceReturnType } from "./useBalance" export { useCreateKernelClientSocial, type UseCreateKernelClientSocialParameters, - type UseCreateKernelClientSocialReturnType, - type CreateKernelClientSocialReturnType + type UseCreateKernelClientSocialReturnType } from "./useCreateKernelClientSocial" + +export { + useConfig, + type UseConfigReturnType +} from "./useConfig" + +export { + useChainId, + type UseChainIdReturnType +} from "./useChainId" + +export { + useSwitchChain, + type UseSwitchChainParameters, + type UseSwitchChainReturnType +} from "./useSwitchChain" + +export { + useChains, + type UseChainsReturnType +} from "./useChains" diff --git a/src/hooks/useBalance.ts b/src/hooks/useBalance.ts index 9e4dacf..9e83b3f 100644 --- a/src/hooks/useBalance.ts +++ b/src/hooks/useBalance.ts @@ -1,182 +1,48 @@ +import type { Evaluate } from "@wagmi/core/internal" +import type { GetBalanceErrorType } from "../actions/getBalance" import { - type QueryFunction, - type QueryFunctionContext, - type UseQueryResult, - useQuery -} from "@tanstack/react-query" + type GetBalanceData, + type GetBalanceOptions, + type GetBalanceQueryFnData, + type GetBalanceQueryKey, + getBalanceQueryOption +} from "../query/getBalance" import { - http, - type Address, - type Chain, - ContractFunctionExecutionError, - type Hex, - type PublicClient, - createPublicClient, - formatUnits, - hexToString, - trim -} from "viem" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" -import { ZERODEV_BUNDLER_URL } from "../utils/constants" + type QueryParameter, + type UseQueryReturnType, + useQuery +} from "../types/query" +import { useConfig } from "./useConfig" import { useKernelClient } from "./useKernelClient" -export type UseBalanceParameters = { - address?: Address - tokenAddress?: Address -} - -export type GetTokenBalanceParameters = { - symbolType: "string" | "bytes32" - publicClient: PublicClient - address: Address - tokenAddress: Address -} - -export type BalanceKey = [ - key: string, - params: { - appId: string | undefined | null - chain: Chain - parameters: UseBalanceParameters - } -] - -export type GetBalanceReturnType = { - value: bigint - decimals: number - symbol: string - formatted: string -} - -export type UseBalanceReturnType = UseQueryResult - -async function getTokenBalance( - parameters: GetTokenBalanceParameters -): Promise { - const { tokenAddress, address, symbolType, publicClient } = parameters - const contract = { - abi: [ - { - type: "function", - name: "balanceOf", - stateMutability: "view", - inputs: [{ type: "address" }], - outputs: [{ type: "uint256" }] - }, - { - type: "function", - name: "decimals", - stateMutability: "view", - inputs: [], - outputs: [{ type: "uint8" }] - }, - { - type: "function", - name: "symbol", - stateMutability: "view", - inputs: [], - outputs: [{ type: symbolType }] - } - ], - address: tokenAddress - } - - const [value, decimals, symbol] = await Promise.all( - [ - { - ...contract, - functionName: "balanceOf", - args: [address] - }, - { ...contract, functionName: "decimals" }, - { ...contract, functionName: "symbol" } - ].map((contract) => publicClient.readContract(contract)) - ) - const formatted = formatUnits(value as bigint, decimals as number) - - return { - decimals: decimals as number, - formatted, - symbol: symbol as string, - value: value as bigint - } -} - -async function getBalanceQueryFn({ - queryKey -}: QueryFunctionContext): Promise { - const [_key, { parameters, appId, chain }] = queryKey - const { address, tokenAddress } = parameters - - if (!address) { - throw new Error("Address is required") - } - if (!appId) { - throw new Error("appId is required") - } - const publicClient = createPublicClient({ - chain: chain, - transport: http(`${ZERODEV_BUNDLER_URL}/${appId}`) - }) - if (tokenAddress) { - try { - return await getTokenBalance({ - address: address, - publicClient, - symbolType: "string", - tokenAddress - }) - } catch (error) { - // In the chance that there is an error upon decoding the contract result, - // it could be likely that the contract data is represented as bytes32 instead - // of a string. - if (error instanceof ContractFunctionExecutionError) { - const balance = await getTokenBalance({ - address: address, - publicClient, - symbolType: "bytes32", - tokenAddress - }) - const symbol = hexToString( - trim(balance.symbol as Hex, { dir: "right" }) - ) - return { ...balance, symbol } - } - throw error - } - } - const balance = await publicClient.getBalance({ - address: address - }) - - return { - value: balance, - decimals: chain.nativeCurrency.decimals, - symbol: chain.nativeCurrency.symbol, - formatted: formatUnits(balance, chain.nativeCurrency.decimals) - } -} - -export function useBalance( - parameters: UseBalanceParameters = {} -): UseBalanceReturnType { - const { appId, chain } = useZeroDevConfig() +export type UseBalanceParameters = Evaluate< + GetBalanceOptions & + QueryParameter< + GetBalanceQueryFnData, + GetBalanceErrorType, + selectData, + GetBalanceQueryKey + > +> + +export type UseBalanceReturnType = + UseQueryReturnType + +export function useBalance( + parameters: UseBalanceParameters = {} +): UseBalanceReturnType { + const { address, query = {} } = parameters const { address: kernelAddress } = useKernelClient() + const config = useConfig() + const client = config.getClient({ chainId: config.state.chainId }) - if (!parameters.address) { - parameters.address = kernelAddress - } + const accountAddress = address ?? kernelAddress - return useQuery({ - queryKey: [ - "balance", - { - parameters, - appId, - chain - } - ], - queryFn: getBalanceQueryFn as unknown as QueryFunction, - enabled: !!appId || !!chain + const options = getBalanceQueryOption(client, { + ...parameters, + address: accountAddress }) + const enabled = Boolean(accountAddress && (query.enabled ?? true)) + + return useQuery({ ...query, ...options, enabled }) } diff --git a/src/hooks/useChainId.ts b/src/hooks/useChainId.ts new file mode 100644 index 0000000..e6e5c6d --- /dev/null +++ b/src/hooks/useChainId.ts @@ -0,0 +1,23 @@ +"use client" + +import { type GetChainIdReturnType, getChainId } from "../actions/getChainId" +import { watchChainId } from "../actions/watchChainId" +import type { Config } from "../createConfig" +import { useConfig } from "./useConfig" + +import { useSyncExternalStore } from "react" + +export type UseChainIdReturnType = + GetChainIdReturnType + +export function useChainId< + TConfig extends Config = Config +>(): UseChainIdReturnType { + const config = useConfig() + + return useSyncExternalStore( + (onChange) => watchChainId(config, { onChange }), + () => getChainId(config), + () => getChainId(config) + ) +} diff --git a/src/hooks/useChains.ts b/src/hooks/useChains.ts new file mode 100644 index 0000000..5201200 --- /dev/null +++ b/src/hooks/useChains.ts @@ -0,0 +1,23 @@ +"use client" + +import { useSyncExternalStore } from "react" +import { type GetChainsReturnType, getChains } from "../actions/getChains" +import { watchChains } from "../actions/watchChains" +import type { Config } from "../createConfig" +import { useConfig } from "./useConfig.js" + +export type UseChainsReturnType = + GetChainsReturnType + +/** https://wagmi.sh/react/api/hooks/useChains */ +export function useChains< + TConfig extends Config = Config +>(): UseChainsReturnType { + const config = useConfig() + + return useSyncExternalStore( + (onChange) => watchChains(config, { onChange }), + () => getChains(config), + () => getChains(config) + ) +} diff --git a/src/hooks/useConfig.ts b/src/hooks/useConfig.ts new file mode 100644 index 0000000..2838ff1 --- /dev/null +++ b/src/hooks/useConfig.ts @@ -0,0 +1,14 @@ +"use client" + +import { useContext } from "react" +import type { Config } from "../createConfig" +import { ZerodevNotConfiguredError } from "../errors" +import { ZeroDevConfigContext } from "../providers/ZeroDevConfigContext" + +export type UseConfigReturnType = config + +export function useConfig(): UseConfigReturnType { + const { config } = useContext(ZeroDevConfigContext) + if (!config) throw new ZerodevNotConfiguredError() + return config +} diff --git a/src/hooks/useCreateBasicSession.ts b/src/hooks/useCreateBasicSession.ts index e262bbd..f705b60 100644 --- a/src/hooks/useCreateBasicSession.ts +++ b/src/hooks/useCreateBasicSession.ts @@ -1,127 +1,73 @@ import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import type { KernelValidator } from "@zerodev/sdk" -import type { Permission } from "@zerodev/session-key" -import { ENTRYPOINT_ADDRESS_V06 } from "permissionless" -import type { EntryPoint } from "permissionless/types" -import { useMemo } from "react" -import type { Abi, PublicClient } from "viem" -import { privateKeyToAccount } from "viem/accounts" +import type { Evaluate } from "@wagmi/core/internal" +import type { CreateBasicSessionErrorType } from "../actions/createBasicSession" import { useUpdateSession } from "../providers/SessionContext" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" import { useKernelAccount } from "../providers/ZeroDevValidatorContext" -import { createSessionKernelAccount } from "../utils/sessions/createSessionKernelAccount" -import { createSessionKey } from "../utils/sessions/manageSession" +import { + type CreateBasicSessionData, + type CreateBasicSessionMutate, + type CreateBasicSessionMutateAsync, + type CreateBasicSessionVariables, + createBasicSessionMutationOptions +} from "../query/createBasicSession" +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" +import { useConfig } from "./useConfig" -export type CreateBasicSessionVariables = { - permissions: Permission[] -} - -export type UseCreateBasicSessionKey = { - validator: KernelValidator | null - permissions: Permission[] | undefined - client: PublicClient | undefined | null - entryPoint: EntryPoint | null -} - -export type UseCreateBasicSessionReturnType = { - write: ({ permissions }: CreateBasicSessionVariables) => void -} & Omit< - UseMutationResult< - CreateBasicSessionReturnType, - unknown, - UseCreateBasicSessionKey, - unknown - >, - "mutate" -> - -export type CreateBasicSessionReturnType = { - sessionKey: `0x${string}` - sessionId: `0x${string}` - smartAccount: `0x${string}` - enableSignature: `0x${string}` - permissions: Permission[] -} - -function mutationKey({ ...config }: UseCreateBasicSessionKey) { - const { permissions, client, validator, entryPoint } = config - - return [ - { - entity: "CreateSession", - client, - validator, - permissions, - entryPoint - } - ] as const -} - -async function mutationFn( - config: UseCreateBasicSessionKey -): Promise { - const { permissions, validator, client, entryPoint } = config +export type UseCreateBasicSessionParameters = Evaluate<{ + mutation?: + | UseMutationParameters< + CreateBasicSessionData, + CreateBasicSessionErrorType, + CreateBasicSessionVariables, + context + > + | undefined +}> - if (!validator || !client || !entryPoint) { - throw new Error("No validator provided") +export type UseCreateBasicSessionReturnType = Evaluate< + UseMutationReturnType< + CreateBasicSessionData, + CreateBasicSessionErrorType, + CreateBasicSessionVariables, + context + > & { + write: CreateBasicSessionMutate + writeAsync: CreateBasicSessionMutateAsync } - if (entryPoint !== ENTRYPOINT_ADDRESS_V06) { - throw new Error("Only kernel v2 is supported in useCreateBasicSession") - } - if (!permissions) { - throw new Error("No permissions provided") - } - - const sessionKey = createSessionKey() - const sessionSigner = privateKeyToAccount(sessionKey) - - const kernelAccount = await createSessionKernelAccount({ - sessionSigner, - publicClient: client, - sudoValidator: validator, - entryPoint: entryPoint, - permissions: permissions - }) - return { - sessionKey, - ...kernelAccount - } -} +> -export function useCreateBasicSession(): UseCreateBasicSessionReturnType { - const { validator, entryPoint } = useKernelAccount() - const { client } = useZeroDevConfig() +export function useCreateBasicSession( + parameters: UseCreateBasicSessionParameters = {} +): UseCreateBasicSessionReturnType { + const { mutation } = parameters + const { entryPoint, validator } = useKernelAccount() + const config = useConfig() const { updateSession } = useUpdateSession() - const { mutate, ...result } = useMutation({ - mutationKey: mutationKey({ - client, - validator, - permissions: undefined, - entryPoint - }), - mutationFn, - onSuccess: (data) => { + const mutatoinOptions = createBasicSessionMutationOptions( + entryPoint, + validator, + config + ) + + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutatoinOptions, + onSuccess: (data, variables, context) => { updateSession({ ...data, policies: [] }) + mutation?.onSuccess?.(data, variables, context) } }) - const write = useMemo(() => { - return ({ permissions }: CreateBasicSessionVariables) => - mutate({ - permissions, - client, - validator, - entryPoint - }) - }, [mutate, validator, client, entryPoint]) - return { ...result, - isPending: !client || result.isPending, - write + write: mutate, + writeAsync: mutateAsync } } diff --git a/src/hooks/useCreateKernelClientEOA.ts b/src/hooks/useCreateKernelClientEOA.ts index b02cd52..6e4f58e 100644 --- a/src/hooks/useCreateKernelClientEOA.ts +++ b/src/hooks/useCreateKernelClientEOA.ts @@ -1,145 +1,83 @@ -import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import { connect, getAccount, getWalletClient } from "@wagmi/core" -import { signerToEcdsaValidator } from "@zerodev/ecdsa-validator" -import { - type KernelSmartAccount, - type KernelValidator, - createKernelAccount -} from "@zerodev/sdk" -import { walletClientToSmartAccountSigner } from "permissionless" -import type { EntryPoint } from "permissionless/types" -import { useMemo } from "react" -import type { PublicClient } from "viem" -import { - type Config, - type Connector, - type CreateConnectorFn, - useConfig -} from "wagmi" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" +import { useConfig } from "wagmi" +import type { CreateKernelClientEOAErrorType } from "../actions/createKernelClientEOA" import { useSetKernelAccount } from "../providers/ZeroDevValidatorContext" +import { + type CreateKernelClientEOAData, + type CreateKernelClientEOAMutate, + type CreateKernelClientEOAMutateAsync, + type CreateKernelClientEOAVariables, + createKernelClientEOAMutationOptions +} from "../query/createKernelClientEOA" import type { KernelVersionType } from "../types" -import { getEntryPointFromVersion } from "../utils/entryPoint" - -export type UseCreateKernelClientEOAParameters = { - version: KernelVersionType -} - -export type CreateKernelClientEOAVariables = { - connector: Connector | CreateConnectorFn -} - -export type UseCreateKernelClientEOAKey = { - connector: Connector | CreateConnectorFn | null | undefined - wagmiConfig: Config | undefined | null - publicClient: PublicClient | undefined | null - version: KernelVersionType -} - -export type CreateKernelClientEOAReturnType = { - validator: KernelValidator - kernelAccount: KernelSmartAccount - entryPoint: EntryPoint -} - -export type UseCreateKernelClientEOAReturnType = { - connect: ({ connector }: CreateKernelClientEOAVariables) => void -} & Omit< - UseMutationResult< - CreateKernelClientEOAReturnType, - unknown, - UseCreateKernelClientEOAKey, - unknown - >, - "mutate" -> - -function mutationKey({ ...config }: UseCreateKernelClientEOAKey) { - const { connector, wagmiConfig } = config - - return [ - { - entity: "CreateKernelClient", - connector, - wagmiConfig - } - ] as const -} - -async function mutationFn( - config: UseCreateKernelClientEOAKey -): Promise { - const { wagmiConfig, connector, publicClient, version } = config - - if (!wagmiConfig || !connector || !publicClient) { - throw new Error("missing config and connector") +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" +import { useConfig as useZdConfig } from "./useConfig" + +export type UseCreateKernelClientEOAParameters = Evaluate< + { + mutation?: + | UseMutationParameters< + CreateKernelClientEOAData, + CreateKernelClientEOAErrorType, + CreateKernelClientEOAVariables, + context + > + | undefined + } & { + version: KernelVersionType } - const entryPoint = getEntryPointFromVersion(version) - - const { status } = getAccount(wagmiConfig) +> - const isConnected = - "uid" in connector && connector.uid === wagmiConfig.state.current - if (status === "disconnected" && !isConnected) { - await connect(wagmiConfig, { connector }) +export type UseCreateKernelClientEOAReturnType = Evaluate< + UseMutationReturnType< + CreateKernelClientEOAData, + CreateKernelClientEOAErrorType, + CreateKernelClientEOAVariables, + context + > & { + connect: CreateKernelClientEOAMutate + connectAsync: CreateKernelClientEOAMutateAsync } - const walletClient = await getWalletClient(wagmiConfig) - const ecdsaValidator = await signerToEcdsaValidator(publicClient, { - entryPoint: entryPoint, - signer: walletClientToSmartAccountSigner(walletClient) - }) - const account = await createKernelAccount(publicClient, { - entryPoint: entryPoint, - plugins: { - sudo: ecdsaValidator - } - }) +> - return { validator: ecdsaValidator, kernelAccount: account, entryPoint } -} +export function useCreateKernelClientEOA( + parameters: UseCreateKernelClientEOAParameters = { version: "v3" } +): UseCreateKernelClientEOAReturnType { + const { mutation, version } = parameters + const config = useConfig() + const zdConfig = useZdConfig() -export function useCreateKernelClientEOA({ - version -}: UseCreateKernelClientEOAParameters): UseCreateKernelClientEOAReturnType { const { setValidator, setKernelAccount, setEntryPoint, setKernelAccountClient } = useSetKernelAccount() - const config = useConfig() - const { client } = useZeroDevConfig() - const { data, mutate, ...result } = useMutation({ - mutationKey: mutationKey({ - wagmiConfig: config, - connector: undefined, - publicClient: client, - version - }), - mutationFn, - onSuccess: (data) => { + const mutationOptions = createKernelClientEOAMutationOptions( + config, + zdConfig, + version + ) + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions, + onSuccess: (data, variables, context) => { setValidator(data.validator) setKernelAccount(data.kernelAccount) setEntryPoint(data.entryPoint) setKernelAccountClient(null) + mutation?.onSuccess?.(data, variables, context) } }) - const connect = useMemo(() => { - return ({ connector }: CreateKernelClientEOAVariables) => - mutate({ - connector, - wagmiConfig: config, - publicClient: client, - version - }) - }, [config, mutate, client, version]) - return { ...result, - data, - connect, - isPending: !client || result.isPending + connect: mutate, + connectAsync: mutateAsync } } diff --git a/src/hooks/useCreateKernelClientPasskey.ts b/src/hooks/useCreateKernelClientPasskey.ts index a051750..b432e86 100644 --- a/src/hooks/useCreateKernelClientPasskey.ts +++ b/src/hooks/useCreateKernelClientPasskey.ts @@ -1,168 +1,118 @@ -import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import { - createPasskeyValidator, - getPasskeyValidator -} from "@zerodev/passkey-validator" -import { - type KernelSmartAccount, - type KernelValidator, - createKernelAccount -} from "@zerodev/sdk" -import type { EntryPoint } from "permissionless/types" +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" import { useMemo } from "react" -import type { PublicClient } from "viem" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" +import type { CreateKernelClientPasskeyErrorType } from "../actions/createKernelClientPasskey" import { useSetKernelAccount } from "../providers/ZeroDevValidatorContext" +import { + type CreateKernelClientPasskeyData, + type CreateKernelClientPasskeyLoginMutate, + type CreateKernelClientPasskeyLoginMutateAsync, + type CreateKernelClientPasskeyRegisterMutate, + type CreateKernelClientPasskeyRegisterMutateAsync, + type CreateKernelClientPasskeyVariables, + createKernelClientPasskeyOptions +} from "../query/createKernelClientPasskey" import type { KernelVersionType } from "../types" -import { ZERODEV_PASSKEY_URL } from "../utils/constants" -import { getEntryPointFromVersion } from "../utils/entryPoint" -import { getWeb3AuthNValidatorFromVersion } from "../utils/webauthn" - -type PasskeConnectType = "register" | "login" - -export type UseCreateKernelClientPasskeyParameters = { - version: KernelVersionType -} -export type CreateKernelClientPasskeyVariables = { - username: string -} - -export type UseCreateKernelClientPasskeyKey = { - username: string | undefined - publicClient: PublicClient | undefined | null - appId: string | undefined | null - type: PasskeConnectType | undefined | null - version: KernelVersionType -} - -export type CreateKernelClientPasskeyReturnType = { - validator: KernelValidator - kernelAccount: KernelSmartAccount - entryPoint: EntryPoint -} - -export type UseCreateKernelClientPasskeyReturnType = { - connectRegister: ({ username }: CreateKernelClientPasskeyVariables) => void - connectLogin: () => void -} & Omit< - UseMutationResult< - CreateKernelClientPasskeyReturnType, - unknown, - UseCreateKernelClientPasskeyKey, - unknown - >, - "mutate" -> - -function mutationKey({ ...config }: UseCreateKernelClientPasskeyKey) { - const { username, publicClient, appId, type } = config - - return [ +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" +import { useConfig } from "./useConfig" + +export type UseCreateKernelClientPasskeyParameters = + Evaluate< { - entity: "CreateKernelClient", - username, - publicClient, - appId, - type - } - ] as const -} - -async function mutationFn( - config: UseCreateKernelClientPasskeyKey -): Promise { - const { username, publicClient, appId, type, version } = config - - if (!publicClient || !appId) { - throw new Error("missing publicClient or appId") - } - let passkeyValidator: KernelValidator - const entryPoint = getEntryPointFromVersion(version) - const webauthnValidator = getWeb3AuthNValidatorFromVersion(version) - - if (type === "register") { - if (!username) { - throw new Error("missing username") + mutation?: + | UseMutationParameters< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelClientPasskeyVariables, + context + > + | undefined + } & { + version: KernelVersionType } - passkeyValidator = await createPasskeyValidator(publicClient, { - passkeyName: username, - passkeyServerUrl: `${ZERODEV_PASSKEY_URL}/${appId}`, - entryPoint: entryPoint, - validatorAddress: webauthnValidator - }) - } else { - passkeyValidator = await getPasskeyValidator(publicClient, { - passkeyServerUrl: `${ZERODEV_PASSKEY_URL}/${appId}`, - entryPoint: entryPoint, - validatorAddress: webauthnValidator - }) - } - - const kernelAccount = await createKernelAccount(publicClient, { - entryPoint: entryPoint, - plugins: { - sudo: passkeyValidator + > + +export type UseCreateKernelClientPasskeyReturnType = + Evaluate< + UseMutationReturnType< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelClientPasskeyVariables, + context + > & { + connectRegister: CreateKernelClientPasskeyRegisterMutate + connectRegisterAsync: CreateKernelClientPasskeyRegisterMutateAsync + connectLogin: CreateKernelClientPasskeyLoginMutate + connectLoginAsync: CreateKernelClientPasskeyLoginMutateAsync } - }) - - return { validator: passkeyValidator, kernelAccount, entryPoint } -} + > -export function useCreateKernelClientPasskey({ - version -}: UseCreateKernelClientPasskeyParameters): UseCreateKernelClientPasskeyReturnType { +export function useCreateKernelClientPasskey( + parameters: UseCreateKernelClientPasskeyParameters = { + version: "v3" + } +): UseCreateKernelClientPasskeyReturnType { + const { mutation, version } = parameters + const config = useConfig() const { setValidator, setKernelAccount, setEntryPoint, setKernelAccountClient } = useSetKernelAccount() - const { appId, client } = useZeroDevConfig() - const { data, mutate, ...result } = useMutation({ - mutationKey: mutationKey({ - appId: appId, - publicClient: client, - username: undefined, - type: undefined, - version - }), - mutationFn, - onSuccess: (data) => { + const mutationOptions = createKernelClientPasskeyOptions(config, version) + + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions, + onSuccess: (data, variables, context) => { setValidator(data.validator) setKernelAccount(data.kernelAccount) setEntryPoint(data.entryPoint) setKernelAccountClient(null) + mutation?.onSuccess?.(data, variables, context) } }) const connectRegister = useMemo(() => { - return ({ username }: CreateKernelClientPasskeyVariables) => + return ({ username }: { username?: string | undefined }) => mutate({ - appId, - publicClient: client, username, - version, type: "register" }) - }, [appId, mutate, client, version]) + }, [mutate]) + + const connectRegisterAsync = useMemo(() => { + return ({ username }: { username?: string | undefined }) => + mutateAsync({ + username, + type: "register" + }) + }, [mutateAsync]) const connectLogin = useMemo(() => { return () => mutate({ - appId, - publicClient: client, - username: undefined, - type: "login", - version + type: "login" + }) + }, [mutate]) + + const connectLoginAsync = useMemo(() => { + return () => + mutateAsync({ + type: "login" }) - }, [appId, mutate, client, version]) + }, [mutateAsync]) return { ...result, - data, - isPending: !client || result.isPending, - connectRegister, - connectLogin + connectRegister: connectRegister, + connectRegisterAsync: connectRegisterAsync, + connectLogin: connectLogin, + connectLoginAsync: connectLoginAsync } } diff --git a/src/hooks/useCreateKernelClientSocial.ts b/src/hooks/useCreateKernelClientSocial.ts index 5192a76..48074d9 100644 --- a/src/hooks/useCreateKernelClientSocial.ts +++ b/src/hooks/useCreateKernelClientSocial.ts @@ -4,19 +4,15 @@ import { type KernelValidator, createKernelAccount } from "@zerodev/sdk" -import { - getSocialValidator, - initiateLogin, - isAuthorized -} from "@zerodev/social-validator" +import { getSocialValidator, isAuthorized } from "@zerodev/social-validator" import type { EntryPoint } from "permissionless/types" -import { useCallback, useEffect, useState } from "react" -import type { PublicClient } from "viem" +import { useCallback, useEffect } from "react" +import type { Config } from "../createConfig" import { useSocial } from "../providers/SocialContext" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" import { useSetKernelAccount } from "../providers/ZeroDevValidatorContext" import type { KernelVersionType } from "../types" import { getEntryPointFromVersion } from "../utils/entryPoint" +import { useConfig } from "./useConfig" export type UseCreateKernelClientSocialParameters = { version: KernelVersionType @@ -26,8 +22,7 @@ export type UseCreateKernelClientSocialParameters = { export type UseCreateKernelClientSocialKey = { version: KernelVersionType oauthCallbackUrl?: string - publicClient?: PublicClient - appId?: string + config: Config type?: "getSocialValidator" } @@ -50,14 +45,13 @@ export type UseCreateKernelClientSocialReturnType = { > function mutationKey(config: UseCreateKernelClientSocialKey) { - const { oauthCallbackUrl, publicClient, appId } = config + const { oauthCallbackUrl, config: zdConfig } = config return [ { entity: "CreateKernelClient", oauthCallbackUrl, - publicClient, - appId + config: zdConfig } ] as const } @@ -65,22 +59,21 @@ function mutationKey(config: UseCreateKernelClientSocialKey) { async function mutationFn( config: UseCreateKernelClientSocialKey ): Promise { - const { publicClient, appId, version, type } = config + const { config: zdConfig, version, type } = config + const projectId = zdConfig.projectIds[zdConfig.state.chainId] + const publicClient = zdConfig.getClient({ chainId: zdConfig.state.chainId }) - if (!appId || !(await isAuthorized({ projectId: appId }))) { + if (!(await isAuthorized({ projectId }))) { throw new Error("Not authorized") } - if (!publicClient || !appId) { - throw new Error("missing publicClient or appId") - } let socialValidator: KernelValidator const entryPoint = getEntryPointFromVersion(version) if (type === "getSocialValidator") { socialValidator = await getSocialValidator(publicClient, { entryPoint, - projectId: appId + projectId }) } else { throw new Error("invalid type") @@ -92,6 +85,25 @@ async function mutationFn( sudo: socialValidator } }) + const uid = `social:${kernelAccount.address}` + zdConfig.setState((x) => { + const chainId = x.chainId + return { + ...x, + connections: new Map(x.connections).set(uid, { + chainId, + accounts: [ + { + client: null, + account: kernelAccount, + entryPoint, + validator: socialValidator + } + ] + }), + current: uid + } + }) return { validator: socialValidator, kernelAccount, entryPoint } } @@ -106,7 +118,7 @@ export function useCreateKernelClientSocial({ setEntryPoint, setKernelAccountClient } = useSetKernelAccount() - const { appId, client } = useZeroDevConfig() + const config = useConfig() const { setIsSocialPending, isSocialPending, @@ -115,8 +127,7 @@ export function useCreateKernelClientSocial({ const { data, mutate, ...result } = useMutation({ mutationKey: mutationKey({ - appId: appId ?? undefined, - publicClient: client ?? undefined, + config, type: undefined, version, oauthCallbackUrl @@ -146,15 +157,14 @@ export function useCreateKernelClientSocial({ useEffect(() => { const load = async () => { mutate({ - appId: appId ?? undefined, - publicClient: client ?? undefined, + config, version, type: "getSocialValidator", oauthCallbackUrl }) } load() - }, [appId, mutate, client, version, oauthCallbackUrl]) + }, [mutate, version, oauthCallbackUrl, config]) return { ...result, diff --git a/src/hooks/useCreateSession.ts b/src/hooks/useCreateSession.ts index d148f1a..24b84ee 100644 --- a/src/hooks/useCreateSession.ts +++ b/src/hooks/useCreateSession.ts @@ -1,127 +1,73 @@ -import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import type { Policy } from "@zerodev/permissions" -import type { KernelValidator } from "@zerodev/sdk" -import { ENTRYPOINT_ADDRESS_V07 } from "permissionless" -import type { EntryPoint } from "permissionless/types" -import { useMemo } from "react" -import type { PublicClient } from "viem" -import { privateKeyToAccount } from "viem/accounts" +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" +import type { CreateSessionErrorType } from "../actions/createSession" import { useUpdateSession } from "../providers/SessionContext" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" import { useKernelAccount } from "../providers/ZeroDevValidatorContext" -import { createSessionKernelAccount } from "../utils/sessions/createSessionKernelAccount" -import { createSessionKey } from "../utils/sessions/manageSession" +import { + type CreateSessionData, + type CreateSessionMutate, + type CreateSessionMutateAsync, + type CreateSessionVariables, + createSessionMutationOptions +} from "../query/createSession" +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" +import { useConfig } from "./useConfig" -export type CreateSessionVariables = { - policies: Policy[] -} - -export type UseCreateSessionKey = { - validator: KernelValidator | null - policies: Policy[] | undefined - client: PublicClient | undefined | null - entryPoint: EntryPoint | null -} - -export type CreateSessionReturnType = { - sessionKey: `0x${string}` - sessionId: `0x${string}` - smartAccount: `0x${string}` - enableSignature: `0x${string}` - policies: Policy[] -} - -export type UseCreateSessionReturnType = { - write: ({ policies }: CreateSessionVariables) => void -} & Omit< - UseMutationResult< - CreateSessionReturnType, - unknown, - UseCreateSessionKey, - unknown - >, - "mutate" -> - -function mutationKey({ ...config }: UseCreateSessionKey) { - const { policies, client, validator, entryPoint } = config - - return [ - { - entity: "CreateSession", - client, - validator, - policies, - entryPoint - } - ] as const -} - -async function mutationFn( - config: UseCreateSessionKey -): Promise { - const { policies, validator, client, entryPoint } = config +export type UseCreateSessionParameters = Evaluate<{ + mutation?: + | UseMutationParameters< + CreateSessionData, + CreateSessionErrorType, + CreateSessionVariables, + context + > + | undefined +}> - if (!validator || !client || !entryPoint) { - throw new Error("No validator provided") +export type UseCreateSessionReturnType = Evaluate< + UseMutationReturnType< + CreateSessionData, + CreateSessionErrorType, + CreateSessionVariables, + context + > & { + write: CreateSessionMutate + writeAsync: CreateSessionMutateAsync } - if (entryPoint !== ENTRYPOINT_ADDRESS_V07) { - throw new Error("Only kernel v3 is supported in useCreateSession") - } - if (!policies) { - throw new Error("No policies provided") - } - - const sessionKey = createSessionKey() - const sessionSigner = privateKeyToAccount(sessionKey) - - const kernelAccount = await createSessionKernelAccount({ - sessionSigner, - publicClient: client, - sudoValidator: validator, - entryPoint: entryPoint, - policies: policies - }) - return { - sessionKey, - ...kernelAccount - } -} +> -export function useCreateSession(): UseCreateSessionReturnType { +export function useCreateSession( + parameters: UseCreateSessionParameters = {} +): UseCreateSessionReturnType { + const { mutation } = parameters const { validator, entryPoint } = useKernelAccount() - const { client } = useZeroDevConfig() + const config = useConfig() const { updateSession } = useUpdateSession() - const { mutate, ...result } = useMutation({ - mutationKey: mutationKey({ - client, - validator, - policies: undefined, - entryPoint - }), - mutationFn, - onSuccess: (data) => { + const mutationOptions = createSessionMutationOptions( + entryPoint, + validator, + config + ) + + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions, + onSuccess: (data, varialbes, context) => { updateSession({ ...data, permissions: [] }) + mutation?.onSuccess?.(data, varialbes, context) } }) - const write = useMemo(() => { - return ({ policies }: CreateSessionVariables) => - mutate({ - policies, - client, - validator, - entryPoint - }) - }, [mutate, validator, client, entryPoint]) - return { ...result, - isPending: !client || result.isPending, - write + write: mutate, + writeAsync: mutateAsync } } diff --git a/src/hooks/useDisconnectKernelClient.ts b/src/hooks/useDisconnectKernelClient.ts index 7984665..ec24a15 100644 --- a/src/hooks/useDisconnectKernelClient.ts +++ b/src/hooks/useDisconnectKernelClient.ts @@ -1,141 +1,72 @@ -import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import type { - KernelAccountClient, - KernelSmartAccount, - KernelValidator -} from "@zerodev/sdk" -import type { EntryPoint } from "permissionless/types" +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" import { useMemo } from "react" -import type { Chain, Transport } from "viem" +import type { DisconnectKernelClientErrorType } from "../actions/disconnectKernelClient" import { useSetKernelAccount } from "../providers/ZeroDevValidatorContext" +import { + type DisconnectKernelClientData, + type DisconnectKernelClientMutate, + type DisconnectKernelClientMutateAsync, + type DisconnectKernelClientVariables, + disconnectKernelClientMutationOptions +} from "../query/disconnectKernelClient" +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" import { useDisconnectSocial } from "./useDisconnectSocial" -export type UseDisconnectKernelClientKey = { - setValidator: - | ((validator: KernelValidator | null) => void) - | null - | undefined - setKernelAccount: - | ((kernelAccount: KernelSmartAccount | null) => void) +export type UseDisconnectKernelClientParameters = Evaluate<{ + mutation?: + | UseMutationParameters< + DisconnectKernelClientData, + DisconnectKernelClientErrorType, + DisconnectKernelClientVariables, + context + > | undefined - | null - setEntryPoint: ((entryPoint: EntryPoint | null) => void) | null | undefined - setKernelAccountClient: - | (( - kernelAccountClient: KernelAccountClient< - EntryPoint, - Transport, - Chain, - KernelSmartAccount - > | null - ) => void) - | null - | undefined - logoutSocial: (() => void) | null | undefined -} +}> -export type DisconnectKernelClientReturnType = boolean - -export type UseDisconnectKernelClientReturnType = { - disconnect: () => void -} & Omit< - UseMutationResult< - DisconnectKernelClientReturnType, - unknown, - UseDisconnectKernelClientKey, - unknown - >, - "mutate" +export type UseDisconnectKernelClientReturnType = Evaluate< + UseMutationReturnType< + DisconnectKernelClientData, + DisconnectKernelClientErrorType, + DisconnectKernelClientVariables, + context + > & { + disconnect: DisconnectKernelClientMutate + disconnectAsync: DisconnectKernelClientMutateAsync + } > -function mutationKey({ ...config }: UseDisconnectKernelClientKey) { - const { - setValidator, - setKernelAccount, - setEntryPoint, - setKernelAccountClient - } = config - - return [ - { - entity: "DisconnectKernelClient", - setValidator, - setKernelAccount, - setEntryPoint, - setKernelAccountClient - } - ] as const -} +export function useDisconnectKernelClient( + parameters: UseDisconnectKernelClientParameters = {} +): UseDisconnectKernelClientReturnType { + const { mutation } = parameters + const { disconnectClient } = useSetKernelAccount() + const { logoutSocial } = useDisconnectSocial() -async function mutationFn( - config: UseDisconnectKernelClientKey -): Promise { - const { - setValidator, - setKernelAccount, - setEntryPoint, - setKernelAccountClient, + const mutationOptions = disconnectKernelClientMutationOptions( + disconnectClient, logoutSocial - } = config + ) - if ( - !setValidator || - !setKernelAccount || - !setEntryPoint || - !setKernelAccountClient - ) { - throw new Error("setKernelAccountClient is required") - } - - await logoutSocial?.() - setValidator(null) - setKernelAccount(null) - setEntryPoint(null) - setKernelAccountClient(null) - - return true -} - -export function useDisconnectKernelClient(): UseDisconnectKernelClientReturnType { - const { - setValidator, - setKernelAccount, - setEntryPoint, - setKernelAccountClient - } = useSetKernelAccount() - const { logoutSocial } = useDisconnectSocial() - - const { mutate, ...result } = useMutation({ - mutationKey: mutationKey({ - setValidator: undefined, - setKernelAccount: undefined, - setEntryPoint: undefined, - setKernelAccountClient: undefined, - logoutSocial: undefined - }), - mutationFn + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions }) const disconnect = useMemo(() => { - return () => - mutate({ - setValidator, - setKernelAccount, - setEntryPoint, - setKernelAccountClient, - logoutSocial - }) - }, [ - mutate, - setValidator, - setKernelAccount, - setEntryPoint, - setKernelAccountClient, - logoutSocial - ]) + return (variables = {}) => mutate(variables) + }, [mutate]) + + const disconnectAsync = useMemo(() => { + return (variables = {}) => mutateAsync(variables) + }, [mutateAsync]) return { ...result, - disconnect + disconnect: disconnect, + disconnectAsync: disconnectAsync } } diff --git a/src/hooks/useDisconnectSocial.ts b/src/hooks/useDisconnectSocial.ts index d9f60c8..0c2ff56 100644 --- a/src/hooks/useDisconnectSocial.ts +++ b/src/hooks/useDisconnectSocial.ts @@ -1,19 +1,20 @@ import { logout } from "@zerodev/social-validator" import { useMemo } from "react" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" import { useKernelAccount } from "../providers/ZeroDevValidatorContext" +import { useConfig } from "./useConfig" export function useDisconnectSocial() { const { validator } = useKernelAccount() - const { appId } = useZeroDevConfig() + const config = useConfig() + const projectId = config.projectIds[config.state.chainId] const logoutSocial = useMemo(() => { return async () => { - if (validator?.source === "SocialValidator" && appId) { - await logout({ projectId: appId }) + if (validator?.source === "SocialValidator") { + await logout({ projectId }) } } - }, [validator, appId]) + }, [validator, projectId]) return { logoutSocial } } diff --git a/src/hooks/useKernelClient.ts b/src/hooks/useKernelClient.ts index b0cc5f7..f92885d 100644 --- a/src/hooks/useKernelClient.ts +++ b/src/hooks/useKernelClient.ts @@ -1,197 +1,54 @@ +import type { Evaluate } from "@wagmi/core/internal" +import type { GetKernelClientErrorType } from "../actions/getKernelClient" +import { useKernelAccount } from "../providers/ZeroDevValidatorContext" import { - type QueryFunction, - type QueryFunctionContext, - type UseQueryResult, - useQuery -} from "@tanstack/react-query" -import { - type KernelAccountClient, - type KernelSmartAccount, - createKernelAccountClient, - createZeroDevPaymasterClient, - gasTokenAddresses -} from "@zerodev/sdk" -import type { EntryPoint } from "permissionless/types" + type GetKernelClientData, + type GetKernelClientOptions, + type GetKernelClientQueryFnData, + type GetKernelClientQueryKey, + getKernelClientQueryOption +} from "../query/getKernelClient" import { - http, - type Address, - type Chain, - type PublicClient, - type Transport -} from "viem" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" -import { useKernelAccount } from "../providers/ZeroDevValidatorContext" -import type { - GasTokenChainIdType, - GasTokenType, - PaymasterERC20, - PaymasterSPONSOR -} from "../types" -import { ZERODEV_BUNDLER_URL, ZERODEV_PAYMASTER_URL } from "../utils/constants" - -export type KernelClientKey = [ - key: string, - params: { - appId: string | undefined | null - chain: Chain | null - kernelAccount: KernelSmartAccount | undefined | null - kernelAccountClient: - | KernelAccountClient< - EntryPoint, - Transport, - Chain, - KernelSmartAccount - > - | undefined - | null - publicClient: PublicClient | undefined | null - entryPoint: EntryPoint - parameters: UseKernelClientParameters - } -] - -export type GetKernelClientReturnType = { - address: Address - entryPoint: EntryPoint - kernelAccount: KernelSmartAccount - kernelClient: KernelAccountClient< - EntryPoint, - Transport, - Chain, - KernelSmartAccount + type QueryParameter, + type UseQueryDataReturnType, + useQueryData +} from "../types/query" +import { useChainId } from "./useChainId" +import { useConfig } from "./useConfig" + +export type UseKernelClientParameters = + Evaluate< + GetKernelClientOptions & + QueryParameter< + GetKernelClientQueryFnData, + GetKernelClientErrorType, + selectData, + GetKernelClientQueryKey + > > -} - -export type UseKernelClientParameters = { - paymaster?: PaymasterERC20 | PaymasterSPONSOR -} - -export type UseKernelClientReturnType = { - address: Address | undefined - entryPoint: EntryPoint - kernelAccount: KernelSmartAccount | undefined - kernelClient: - | KernelAccountClient< - EntryPoint, - Transport, - Chain, - KernelSmartAccount - > - | undefined - isConnected: boolean - isLoading: boolean - error: unknown -} & UseQueryResult - -async function getKernelClient({ - queryKey -}: QueryFunctionContext): Promise { - const [ - _key, - { - appId, - publicClient, - kernelAccount, - entryPoint, - chain, - kernelAccountClient, - parameters: { paymaster } - } - ] = queryKey - - if (kernelAccountClient) { - return { - kernelClient: kernelAccountClient, - kernelAccount: kernelAccountClient.account, - address: kernelAccountClient.account.address, - entryPoint: entryPoint - } - } - if (!appId || !chain || !publicClient || !kernelAccount || !entryPoint) { - throw new Error("missing appId or kernelAccount") - } +export type UseKernelClientReturnType = + Evaluate> - const kernelClient = createKernelAccountClient({ - account: kernelAccount, - chain: chain, - bundlerTransport: http(`${ZERODEV_BUNDLER_URL}/${appId}`), - entryPoint: entryPoint, - middleware: !paymaster?.type - ? undefined - : { - sponsorUserOperation: async ({ userOperation }) => { - let gasToken: GasTokenType | undefined - if (paymaster.type === "ERC20") { - const chainId = chain.id as GasTokenChainIdType - if ( - !(chainId in gasTokenAddresses) || - !( - paymaster.gasToken in - gasTokenAddresses[chainId] - ) - ) { - throw new Error("ERC20 token not supported") - } - gasToken = - paymaster.gasToken as keyof (typeof gasTokenAddresses)[typeof chainId] - } - - const kernelPaymaster = createZeroDevPaymasterClient({ - entryPoint: entryPoint, - chain: chain, - transport: http( - `${ZERODEV_PAYMASTER_URL}/${appId}?paymasterProvider=PIMLICO` - ) - }) - return kernelPaymaster.sponsorUserOperation({ - userOperation, - entryPoint: entryPoint, - gasToken: gasToken - }) - } - } - }) as KernelAccountClient< - EntryPoint, - Transport, - Chain, - KernelSmartAccount - > - return { - kernelClient, - kernelAccount, - address: kernelAccount.address, - entryPoint - } -} - -export function useKernelClient( - parameters: UseKernelClientParameters = {} -): UseKernelClientReturnType { - const { appId, chain, client } = useZeroDevConfig() +export function useKernelClient( + parameters: UseKernelClientParameters = {} +): UseKernelClientReturnType { + const { query = {} } = parameters + const config = useConfig() + const chainId = useChainId() const { kernelAccount, entryPoint, kernelAccountClient } = useKernelAccount() - const { data, ...result } = useQuery({ - queryKey: [ - "session_kernel_client", - { - publicClient: client, - parameters, - kernelAccount, - appId, - entryPoint, - chain, - kernelAccountClient - } - ], - queryFn: getKernelClient as unknown as QueryFunction, - enabled: !!client && !!appId && !!entryPoint && !!chain - }) + const options = getKernelClientQueryOption( + config, + kernelAccount, + kernelAccountClient, + entryPoint, + chainId, + { + ...parameters + } + ) - return { - ...data, - isConnected: !!data?.kernelClient, - ...result - } + return useQueryData({ ...query, ...options }) } diff --git a/src/hooks/useSendTransaction.ts b/src/hooks/useSendTransaction.ts index 8b4b753..f77c5d0 100644 --- a/src/hooks/useSendTransaction.ts +++ b/src/hooks/useSendTransaction.ts @@ -1,143 +1,74 @@ -import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import type { SendTransactionParameters } from "@wagmi/core" +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" +import type { SendTransactionErrorType } from "../actions/sendTransaction" import { - type KernelAccountClient, - type KernelSmartAccount, - getCustomNonceKeyFromString -} from "@zerodev/sdk" -import type { EntryPoint } from "permissionless/types" -import { useMemo } from "react" -import { type Hash, encodeFunctionData } from "viem" + type SendTransactionData, + type SendTransactionMutate, + type SendTransactionMutateAsync, + type SendTransactionVariables, + createSendTransactionOptions +} from "../query/sendTransaction" import type { PaymasterERC20, PaymasterSPONSOR } from "../types" -import { generateRandomString } from "../utils" +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" +import { useChainId } from "./useChainId" import { useKernelClient } from "./useKernelClient" -export type SendTransactionVariables = SendTransactionParameters[] - -export type UseSendTransactionParameters = { - paymaster?: PaymasterERC20 | PaymasterSPONSOR - isParallel?: boolean - nonceKey?: string -} - -export type UseSendTransactionKey = { - variables: SendTransactionVariables - kernelClient: KernelAccountClient | undefined | null - kernelAccount: KernelSmartAccount | undefined | null - isParallel: boolean - seed: string - nonceKey: string | undefined -} - -export type SendTransactionReturnType = Hash - -export type UseSendTransactionReturnType = { - write: (variables: SendTransactionVariables) => void -} & Omit< - UseMutationResult< - SendTransactionReturnType, - unknown, - UseSendTransactionKey, - unknown - >, - "mutate" +export type UseSendTransactionParameters = Evaluate< + { + mutation?: + | UseMutationParameters< + SendTransactionData, + SendTransactionErrorType, + SendTransactionVariables, + context + > + | undefined + } & { + paymaster?: PaymasterERC20 | PaymasterSPONSOR + isParallel?: boolean + nonceKey?: string + } > -function mutationKey({ ...config }: UseSendTransactionKey) { - const { - kernelAccount, - kernelClient, - variables, - isParallel, - seed, - nonceKey - } = config +export type UseSendTransactionReturnType = Evaluate< + UseMutationReturnType< + SendTransactionData, + SendTransactionErrorType, + SendTransactionVariables, + context + > & { + isLoading: boolean + write: SendTransactionMutate + writeAsync: SendTransactionMutateAsync + } +> - return [ - { - entity: "sendTransaction", - kernelAccount, - kernelClient, - variables, - isParallel, - seed, - nonceKey - } - ] as const -} +export function useSendTransaction( + parameters: UseSendTransactionParameters = {} +): UseSendTransactionReturnType { + const { isParallel = true, nonceKey, paymaster, mutation } = parameters + const { kernelClient, isPending } = useKernelClient(parameters) + const chainId = useChainId() -async function mutationFn( - config: UseSendTransactionKey -): Promise { - const { - kernelAccount, + const mutationOptions = createSendTransactionOptions( + "sendTransaction", kernelClient, - variables, isParallel, - seed, - nonceKey - } = config - - if (!kernelClient || !kernelAccount) { - throw new Error("Kernel Client is required") - } - - const seedForNonce = nonceKey ? nonceKey : seed - let nonce: bigint | undefined - if (nonceKey || isParallel) { - const customNonceKey = getCustomNonceKeyFromString( - seedForNonce, - kernelAccount.entryPoint - ) - nonce = await kernelAccount.getNonce(customNonceKey) - } - - return kernelClient.sendTransactions({ - transactions: variables.map((p) => ({ - to: p.to, - value: p.value ?? 0n, - data: p.data ?? "0x" - })), - nonce - }) -} - -export function useSendTransaction( - parameters: UseSendTransactionParameters = {} -): UseSendTransactionReturnType { - const { isParallel = true, nonceKey } = parameters - const { kernelAccount, kernelClient, error } = useKernelClient(parameters) - - const seed = useMemo(() => generateRandomString(), []) - - const { mutate, ...result } = useMutation({ - mutationKey: mutationKey({ - kernelClient, - kernelAccount, - variables: {} as SendTransactionVariables, - isParallel: isParallel, - seed, - nonceKey - }), - mutationFn + nonceKey, + chainId, + paymaster + ) + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions }) - - const write = useMemo(() => { - return (variables: SendTransactionVariables) => { - mutate({ - variables, - kernelAccount, - kernelClient, - isParallel: isParallel, - seed: generateRandomString(), - nonceKey - }) - } - }, [mutate, kernelClient, kernelAccount, isParallel, nonceKey]) - return { ...result, - error: error ?? result.error, - write + isLoading: isPending, + write: mutate, + writeAsync: mutateAsync } } diff --git a/src/hooks/useSendTransactionWithSession.ts b/src/hooks/useSendTransactionWithSession.ts new file mode 100644 index 0000000..8803e75 --- /dev/null +++ b/src/hooks/useSendTransactionWithSession.ts @@ -0,0 +1,86 @@ +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" +import type { SendTransactionErrorType } from "../actions/sendTransaction" +import { + type SendTransactionData, + type SendTransactionMutate, + type SendTransactionMutateAsync, + type SendTransactionVariables, + createSendTransactionOptions +} from "../query/sendTransaction" +import type { PaymasterERC20, PaymasterSPONSOR } from "../types" +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" +import { useChainId } from "./useChainId" +import { useSessionKernelClient } from "./useSessionKernelClient" + +export type UseSendTransactionWithSessionParameters = + Evaluate< + { + mutation?: + | UseMutationParameters< + SendTransactionData, + SendTransactionErrorType, + SendTransactionVariables, + context + > + | undefined + } & { + sessionId?: `0x${string}` | null | undefined + paymaster?: PaymasterERC20 | PaymasterSPONSOR + isParallel?: boolean + nonceKey?: string + } + > + +export type UseSendTransactionWithSessionReturnType = + Evaluate< + UseMutationReturnType< + SendTransactionData, + SendTransactionErrorType, + SendTransactionVariables, + context + > & { + isLoading: boolean + write: SendTransactionMutate + writeAsync: SendTransactionMutateAsync + } + > + +export function useSendTransactionWithSession( + parameters: UseSendTransactionWithSessionParameters = {} +): UseSendTransactionWithSessionReturnType { + const { + isParallel = true, + nonceKey, + mutation, + sessionId, + paymaster + } = parameters + const { kernelClient, isPending } = useSessionKernelClient(parameters) + const chainId = useChainId() + + const mutationOptions = createSendTransactionOptions( + "sendTransactionWithSession", + kernelClient, + isParallel, + nonceKey, + chainId, + paymaster, + sessionId + ) + + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions + }) + + return { + ...result, + isLoading: isPending, + write: mutate, + writeAsync: mutateAsync + } +} diff --git a/src/hooks/useSendUserOperation.ts b/src/hooks/useSendUserOperation.ts index e0a6bf6..c875197 100644 --- a/src/hooks/useSendUserOperation.ts +++ b/src/hooks/useSendUserOperation.ts @@ -1,147 +1,76 @@ -import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import type { WriteContractParameters } from "@wagmi/core" +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" +import type { SendUserOperationErrorType } from "../actions/sendUserOperation" import { - type KernelAccountClient, - type KernelSmartAccount, - getCustomNonceKeyFromString -} from "@zerodev/sdk" -import type { EntryPoint } from "permissionless/types" -import { useMemo } from "react" -import { type Hash, encodeFunctionData } from "viem" + type SendUserOperationData, + type SendUserOperationMutate, + type SendUserOperationMutateAsync, + type SendUserOperationVariables, + createSendUserOperationOptions +} from "../query/sendUserOperation" import type { PaymasterERC20, PaymasterSPONSOR } from "../types" -import { generateRandomString } from "../utils" +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" +import { useChainId } from "./useChainId" import { useKernelClient } from "./useKernelClient" -export type SendUserOperationVariables = WriteContractParameters[] - -export type UseSendUserOperationParameters = { - paymaster?: PaymasterERC20 | PaymasterSPONSOR - isParallel?: boolean - nonceKey?: string -} - -export type UseSendUserOperationKey = { - variables: SendUserOperationVariables - kernelClient: KernelAccountClient | undefined | null - kernelAccount: KernelSmartAccount | undefined | null - isParallel: boolean - seed: string - nonceKey: string | undefined -} - -export type SendUserOperationReturnType = Hash - -export type UseSendUserOperationReturnType = { - write: (variables: SendUserOperationVariables) => void -} & Omit< - UseMutationResult< - SendUserOperationReturnType, - unknown, - UseSendUserOperationKey, - unknown - >, - "mutate" +export type UseSendUserOperationParameters = Evaluate< + { + mutation?: + | UseMutationParameters< + SendUserOperationData, + SendUserOperationErrorType, + SendUserOperationVariables, + context + > + | undefined + } & { + paymaster?: PaymasterERC20 | PaymasterSPONSOR + isParallel?: boolean + nonceKey?: string + } > -function mutationKey({ ...config }: UseSendUserOperationKey) { - const { - kernelAccount, - kernelClient, - variables, - isParallel, - seed, - nonceKey - } = config +export type UseSendUserOperationReturnType = Evaluate< + UseMutationReturnType< + SendUserOperationData, + SendUserOperationErrorType, + SendUserOperationVariables, + context + > & { + isLoading: boolean + write: SendUserOperationMutate + writeAsync: SendUserOperationMutateAsync + } +> - return [ - { - entity: "sendUserOperation", - kernelAccount, - kernelClient, - variables, - isParallel, - seed, - nonceKey - } - ] as const -} +export function useSendUserOperation( + parameters: UseSendUserOperationParameters = {} +): UseSendUserOperationReturnType { + const { isParallel = true, nonceKey, paymaster, mutation } = parameters + const { kernelClient, isPending } = useKernelClient(parameters) + const chainId = useChainId() -async function mutationFn( - config: UseSendUserOperationKey -): Promise { - const { - kernelAccount, + const mutationOptions = createSendUserOperationOptions( + "sendUserOperation", kernelClient, - variables, isParallel, - seed, - nonceKey - } = config - - if (!kernelClient || !kernelAccount) { - throw new Error("Kernel Client is required") - } - - const seedForNonce = nonceKey ? nonceKey : seed - let nonce: bigint | undefined - if (nonceKey || isParallel) { - const customNonceKey = getCustomNonceKeyFromString( - seedForNonce, - kernelAccount.entryPoint - ) - nonce = await kernelAccount.getNonce(customNonceKey) - } - - return kernelClient.sendUserOperation({ - userOperation: { - callData: await kernelAccount.encodeCallData( - variables.map((p) => ({ - to: p.address, - value: p.value ?? 0n, - data: encodeFunctionData(p) - })) - ), - nonce - } + nonceKey, + chainId, + paymaster + ) + + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions }) -} - -export function useSendUserOperation( - parameters: UseSendUserOperationParameters = {} -): UseSendUserOperationReturnType { - const { isParallel = true, nonceKey } = parameters - const { kernelAccount, kernelClient, error } = useKernelClient(parameters) - - const seed = useMemo(() => generateRandomString(), []) - - const { mutate, ...result } = useMutation({ - mutationKey: mutationKey({ - kernelClient, - kernelAccount, - variables: {} as SendUserOperationVariables, - isParallel: isParallel, - seed, - nonceKey - }), - mutationFn - }) - - const write = useMemo(() => { - return (variables: SendUserOperationVariables) => { - mutate({ - variables, - kernelAccount, - kernelClient, - isParallel: isParallel, - seed: generateRandomString(), - nonceKey - }) - } - }, [mutate, kernelClient, kernelAccount, isParallel, nonceKey]) return { ...result, - error: error ?? result.error, - write + isLoading: isPending, + write: mutate, + writeAsync: mutateAsync } } diff --git a/src/hooks/useSendUserOperationWithSession.ts b/src/hooks/useSendUserOperationWithSession.ts index 78dddd7..4c016b5 100644 --- a/src/hooks/useSendUserOperationWithSession.ts +++ b/src/hooks/useSendUserOperationWithSession.ts @@ -1,156 +1,86 @@ -import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import type { WriteContractParameters } from "@wagmi/core" +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" +import type { SendUserOperationErrorType } from "../actions/sendUserOperation" import { - type KernelAccountClient, - type KernelSmartAccount, - getCustomNonceKeyFromString -} from "@zerodev/sdk" -import type { EntryPoint } from "permissionless/types" -import { useMemo } from "react" -import { type Hash, encodeFunctionData } from "viem" + type SendUserOperationData, + type SendUserOperationMutate, + type SendUserOperationMutateAsync, + type SendUserOperationVariables, + createSendUserOperationOptions +} from "../query/sendUserOperation" import type { PaymasterERC20, PaymasterSPONSOR } from "../types" -import { generateRandomString } from "../utils" +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" +import { useChainId } from "./useChainId" import { useSessionKernelClient } from "./useSessionKernelClient" -export type UseSendUserOperationWithSessionParameters = { - sessionId?: `0x${string}` | null | undefined - paymaster?: PaymasterERC20 | PaymasterSPONSOR - isParallel?: boolean - nonceKey?: string -} - -export type SendUserOperationWithSessionVariables = WriteContractParameters[] - -export type UseSendUserOperationWithSessionKey = { - variables: SendUserOperationWithSessionVariables - kernelClient: KernelAccountClient | undefined - kernelAccount: KernelSmartAccount | undefined - isParallel: boolean - seed: string - nonceKey: string | undefined -} - -export type SendUserOperationWithSessionReturnType = Hash - -export type UseSendUserOperationWithSessionReturnType = { - isDisabled: boolean - write: (variables: SendUserOperationWithSessionVariables) => void -} & Omit< - UseMutationResult< - SendUserOperationWithSessionReturnType, - unknown, - UseSendUserOperationWithSessionKey, - unknown - >, - "mutate" -> - -function mutationKey({ ...config }: UseSendUserOperationWithSessionKey) { - const { - variables, - kernelClient, - kernelAccount, - isParallel, - seed, - nonceKey - } = config - - return [ +export type UseSendUserOperationWithSessionParameters = + Evaluate< { - entity: "sendUserOperationWithSession", - variables, - kernelClient, - kernelAccount, - isParallel, - seed, - nonceKey + mutation?: + | UseMutationParameters< + SendUserOperationData, + SendUserOperationErrorType, + SendUserOperationVariables, + context + > + | undefined + } & { + sessionId?: `0x${string}` | null | undefined + paymaster?: PaymasterERC20 | PaymasterSPONSOR + isParallel?: boolean + nonceKey?: string } - ] as const -} - -async function mutationFn(config: UseSendUserOperationWithSessionKey) { - const { - variables, - kernelClient, - kernelAccount, - isParallel, - seed, - nonceKey - } = config - - if (!kernelClient || !kernelAccount) { - throw new Error("Kernel Client is required") - } - - const seedForNonce = nonceKey ? nonceKey : seed - let nonce: bigint | undefined - if (nonceKey || isParallel) { - const customNonceKey = getCustomNonceKeyFromString( - seedForNonce, - kernelAccount.entryPoint - ) - nonce = await kernelAccount.getNonce(customNonceKey) - } - - const userOpHash = await kernelClient.sendUserOperation({ - userOperation: { - callData: await kernelAccount.encodeCallData( - variables.map((p) => ({ - to: p.address, - value: p.value ?? 0n, - data: encodeFunctionData(p) - })) - ), - nonce + > + +export type UseSendUserOperationWithSessionReturnType = + Evaluate< + UseMutationReturnType< + SendUserOperationData, + SendUserOperationErrorType, + SendUserOperationVariables, + context + > & { + isLoading: boolean + write: SendUserOperationMutate + writeAsync: SendUserOperationMutateAsync } - }) + > - return userOpHash -} - -export function useSendUserOperationWithSession( - parameters: UseSendUserOperationWithSessionParameters = {} -): UseSendUserOperationWithSessionReturnType { - const { isParallel = true, nonceKey } = parameters +export function useSendUserOperationWithSession( + parameters: UseSendUserOperationWithSessionParameters = {} +): UseSendUserOperationWithSessionReturnType { const { + isParallel = true, + nonceKey, + mutation, + sessionId, + paymaster + } = parameters + const { kernelClient, isPending } = useSessionKernelClient(parameters) + const chainId = useChainId() + + const mutationOptions = createSendUserOperationOptions( + "sendUserOperationWithSession", kernelClient, - kernelAccount, - isLoading, - error: clientError - } = useSessionKernelClient(parameters) - - const seed = useMemo(() => generateRandomString(), []) - - const { mutate, error, ...result } = useMutation({ - mutationKey: mutationKey({ - variables: {} as SendUserOperationWithSessionVariables, - kernelClient, - kernelAccount, - isParallel: isParallel, - seed, - nonceKey - }), - mutationFn + isParallel, + nonceKey, + chainId, + paymaster, + sessionId + ) + + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions }) - const write = useMemo(() => { - return (variables: SendUserOperationWithSessionVariables) => { - mutate({ - variables, - kernelClient, - kernelAccount, - isParallel: isParallel, - seed: generateRandomString(), - nonceKey - }) - } - }, [mutate, kernelClient, kernelAccount, isParallel, nonceKey]) - return { ...result, - isDisabled: !!clientError, - isPending: isLoading || result.isPending, - error: error || clientError, - write + isLoading: isPending, + write: mutate, + writeAsync: mutateAsync } } diff --git a/src/hooks/useSessionKernelClient.ts b/src/hooks/useSessionKernelClient.ts index 0fa29e1..5ae8e37 100644 --- a/src/hooks/useSessionKernelClient.ts +++ b/src/hooks/useSessionKernelClient.ts @@ -1,197 +1,62 @@ +import type { Evaluate } from "@wagmi/core/internal" +import type { GetSessionKernelClientErrorType } from "../actions/getSessionKernelClient" +import { useKernelAccount } from "../providers/ZeroDevValidatorContext" import { - type QueryFunction, - type QueryFunctionContext, - type UseQueryResult, - useQuery -} from "@tanstack/react-query" + type GetSessionKernelClientData, + type GetSessionKernelClientOptions, + type GetSessionKernelClientQueryFnData, + type GetSessionKernelClientQueryKey, + getSessionKernelClientQueryOption +} from "../query/getSessionKernelClient" import { - type KernelAccountClient, - type KernelSmartAccount, - type KernelValidator, - createKernelAccountClient, - createZeroDevPaymasterClient, - gasTokenAddresses -} from "@zerodev/sdk" -import type { EntryPoint } from "permissionless/types" -import { http, type Chain, type PublicClient } from "viem" -import { privateKeyToAccount } from "viem/accounts" -import { useZeroDevConfig } from "../providers/ZeroDevAppContext" -import { useKernelAccount } from "../providers/ZeroDevValidatorContext" -import type { - GasTokenChainIdType, - GasTokenType, - PaymasterERC20, - PaymasterSPONSOR, - SessionType -} from "../types" -import { ZERODEV_BUNDLER_URL, ZERODEV_PAYMASTER_URL } from "../utils/constants" -import { getSessionKernelAccount } from "../utils/sessions/getSessionKernelAccount" + type QueryParameter, + type UseQueryDataReturnType, + useQueryData +} from "../types/query" +import { useChainId } from "./useChainId" +import { useConfig } from "./useConfig" import { useSessions } from "./useSessions" -export type UseSessionKernelClientParameters = { - sessionId?: `0x${string}` | null | undefined - paymaster?: PaymasterERC20 | PaymasterSPONSOR -} - -export type SessionKernelClientKey = [ - key: string, - params: { - appId: string | undefined | null - chain: Chain | null - validator: KernelValidator - kernelAddress: string | undefined | null - publicClient: PublicClient | undefined | null - parameters: UseSessionKernelClientParameters - session: SessionType | undefined - entryPoint: EntryPoint | null - } -] - -export type GetSessionKernelClientReturnType = { - kernelClient: KernelAccountClient - kernelAccount: KernelSmartAccount -} - -export type UseSessionKernelClientReturnType = { - kernelClient: KernelAccountClient - kernelAccount: KernelSmartAccount - isLoading: boolean - error: unknown -} & UseQueryResult - -async function getSessionKernelClient({ - queryKey -}: QueryFunctionContext) { - const [ - _key, - { - appId, - chain, - publicClient, - parameters, - validator, - session, - kernelAddress, - entryPoint - } - ] = queryKey - const { sessionId, paymaster } = parameters - - if (!appId || !chain) { - throw new Error("appId and chain are required") - } - if (!entryPoint) { - throw new Error("entryPoint is required") - } - if (!publicClient) { - throw new Error("publicClient is required") - } - - // get session from sessionId - if (!session) { - throw new Error("session not found") - } - const accountSession = Object.values(session).filter( - (s) => s.smartAccount === kernelAddress - ) - if (accountSession.length === 0) { - throw new Error("No available session for this account") - } - if (accountSession.length > 1 && !sessionId) { - throw new Error("sessionId is required") - } - const selectedSession = sessionId ? session[sessionId] : accountSession[0] - - // create kernelAccountClient - const sessionSigner = privateKeyToAccount(selectedSession.sessionKey) - const { kernelAccount } = await getSessionKernelAccount({ - sessionSigner, - publicClient, - sudoValidator: validator, - entryPoint: entryPoint, - policies: selectedSession.policies, - permissions: selectedSession.permissions, - enableSignature: selectedSession.enableSignature - }) - const kernelClient = createKernelAccountClient({ - account: kernelAccount, - chain: chain, - bundlerTransport: http(`${ZERODEV_BUNDLER_URL}/${appId}`), - entryPoint: entryPoint, - middleware: !paymaster?.type - ? undefined - : { - sponsorUserOperation: async ({ userOperation }) => { - let gasToken: GasTokenType | undefined - - if (paymaster.type === "ERC20") { - const chainId = chain.id as GasTokenChainIdType - if ( - !(chainId in gasTokenAddresses) || - !( - paymaster.gasToken in - gasTokenAddresses[chainId] - ) - ) { - throw new Error("ERC20 token not supported") - } - gasToken = - paymaster.gasToken as keyof (typeof gasTokenAddresses)[typeof chainId] - } - - const kernelPaymaster = createZeroDevPaymasterClient({ - entryPoint: entryPoint, - chain: chain, - transport: http( - `${ZERODEV_PAYMASTER_URL}/${appId}?paymasterProvider=PIMLICO` - ) - }) - return kernelPaymaster.sponsorUserOperation({ - userOperation, - entryPoint: entryPoint, - gasToken: gasToken - }) - } - } - }) - - return { kernelClient, kernelAccount } -} - -export function useSessionKernelClient( - parameters: UseSessionKernelClientParameters = {} -): UseSessionKernelClientReturnType { - const { appId, chain, client } = useZeroDevConfig() +export type UseSessionKernelClientParameters< + selectData = GetSessionKernelClientData +> = Evaluate< + GetSessionKernelClientOptions & + QueryParameter< + GetSessionKernelClientQueryFnData, + GetSessionKernelClientErrorType, + selectData, + GetSessionKernelClientQueryKey + > +> + +export type UseSessionKernelClientReturnType< + selectData = GetSessionKernelClientData +> = Evaluate< + UseQueryDataReturnType +> + +export function useSessionKernelClient( + parameters: UseSessionKernelClientParameters = {} +): UseSessionKernelClientReturnType { + const { query = {} } = parameters + const config = useConfig() + const chainId = useChainId() const { validator, kernelAccount, entryPoint } = useKernelAccount() const session = useSessions() const kernelAddress = kernelAccount?.address - const { data, ...result } = useQuery({ - queryKey: [ - "session_kernel_client", - { - publicClient: client, - kernelAddress, - parameters, - validator, - session, - appId, - chain, - entryPoint - } - ], - queryFn: getSessionKernelClient as unknown as QueryFunction, - enabled: - !!client && - !!validator && - !!appId && - !!kernelAddress && - !!entryPoint && - !!chain - }) + const options = getSessionKernelClientQueryOption( + config, + chainId, + validator, + kernelAddress, + entryPoint, + session, + { + ...parameters + } + ) + const enabled = Boolean(validator && kernelAddress && entryPoint) - return { - ...data, - ...result - } + return useQueryData({ ...query, ...options, enabled }) } diff --git a/src/hooks/useSessions.ts b/src/hooks/useSessions.ts index ae20ddd..1bd5311 100644 --- a/src/hooks/useSessions.ts +++ b/src/hooks/useSessions.ts @@ -1,23 +1,30 @@ import { useContext, useMemo } from "react" import { SessionContext } from "../providers/SessionContext" import type { SessionType } from "../types" +import { useChainId } from "./useChainId" import { useKernelClient } from "./useKernelClient" export type useSessionsReturnType = SessionType | null export function useSessions(): useSessionsReturnType { + const chainId = useChainId() const { address } = useKernelClient() const { sessions } = useContext(SessionContext) const accountSession = useMemo(() => { if (!sessions) return null return Object.entries(sessions) - .filter(([key, session]) => session.smartAccount === address) + .filter( + ([key, session]) => + session.smartAccount === address && + key.endsWith(`:${chainId}`) + ) .reduce((acc: SessionType, [key, session]) => { - acc[key as `0x${string}`] = session + const sessionId = key.split(":")[0] as `0x${string}` + acc[sessionId] = session return acc }, {}) - }, [sessions, address]) + }, [sessions, address, chainId]) return accountSession } diff --git a/src/hooks/useSetKernelClient.ts b/src/hooks/useSetKernelClient.ts index 62fb189..89a03df 100644 --- a/src/hooks/useSetKernelClient.ts +++ b/src/hooks/useSetKernelClient.ts @@ -1,76 +1,60 @@ -import { type UseMutationResult, useMutation } from "@tanstack/react-query" -import type { KernelAccountClient } from "@zerodev/sdk" -import type { EntryPoint } from "permissionless/types" -import { useContext, useMemo } from "react" +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" +import { useContext } from "react" +import type { SetKernelClientErrorType } from "../actions/setKernelClient" import { ZeroDevValidatorContext } from "../providers/ZeroDevValidatorContext" - -export type UseSetKernelClientKey = { - kernelClient: KernelAccountClient | undefined - setKernelAccountClient: (kernelAccountClient: any | null) => void -} - -export type SetKernelClientReturnType = boolean - -export type UseSetKernelClientReturnType = { - setKernelClient: (kernelClient: any) => void -} & Omit< - UseMutationResult< - SetKernelClientReturnType, - unknown, - UseSetKernelClientKey, - unknown - >, - "mutate" -> - -function mutationKey({ ...config }: UseSetKernelClientKey) { - const { kernelClient, setKernelAccountClient } = config - - return [ - { - entity: "SetKernelClient", - kernelClient, - setKernelAccountClient - } - ] as const -} - -async function mutationFn( - config: UseSetKernelClientKey -): Promise { - const { setKernelAccountClient, kernelClient } = config - - if (!kernelClient || !setKernelAccountClient) { - throw new Error("kernelClient is required") +import { + type SetKernelClientData, + type SetKernelClientMutate, + type SetKernelClientMutateAsync, + type SetKernelClientVariables, + setKernelClientMutationOptions +} from "../query/setKernelClient" +import type { + UseMutationParameters, + UseMutationReturnType +} from "../types/query" + +export type UseSetKernelClientParameters = Evaluate<{ + mutation?: + | UseMutationParameters< + SetKernelClientData, + SetKernelClientErrorType, + SetKernelClientVariables, + context + > + | undefined +}> + +export type UseSetKernelClientReturnType = Evaluate< + UseMutationReturnType< + SetKernelClientData, + SetKernelClientErrorType, + SetKernelClientVariables, + context + > & { + setKernelClient: SetKernelClientMutate + setKernelClientAsync: SetKernelClientMutateAsync } +> - setKernelAccountClient(kernelClient) - - return true -} - -export function useSetKernelClient(): UseSetKernelClientReturnType { +export function useSetKernelClient( + parameters: UseSetKernelClientParameters = {} +): UseSetKernelClientReturnType { + const { mutation } = parameters const { setKernelAccountClient } = useContext(ZeroDevValidatorContext) - const { mutate, ...result } = useMutation({ - mutationKey: mutationKey({ - setKernelAccountClient, - kernelClient: undefined - }), - mutationFn + const mutationOptions = setKernelClientMutationOptions( + setKernelAccountClient + ) + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutationOptions, + ...mutation }) - const setKernelClient = useMemo(() => { - return (kernelClient: any) => { - mutate({ - setKernelAccountClient, - kernelClient - }) - } - }, [mutate, setKernelAccountClient]) - return { ...result, - setKernelClient + setKernelClient: mutate, + setKernelClientAsync: mutateAsync } } diff --git a/src/hooks/useSwitchChain.ts b/src/hooks/useSwitchChain.ts new file mode 100644 index 0000000..7025b35 --- /dev/null +++ b/src/hooks/useSwitchChain.ts @@ -0,0 +1,89 @@ +import { useMutation } from "@tanstack/react-query" +import type { Evaluate } from "@wagmi/core/internal" +import { useConfig as useWagmiConfig } from "wagmi" +import type { SwitchChainErrorType } from "../actions/switchChain" +import type { Config as ZdConfig } from "../createConfig" +import { useSetKernelAccount } from "../providers/ZeroDevValidatorContext" +import { useKernelAccount } from "../providers/ZeroDevValidatorContext" +import { + type SwitchChainData, + type SwitchChainMutate, + type SwitchChainMutateAsync, + type SwitchChainVariables, + switchChainMutationOptions +} from "../query/switchChain" +import type { UseMutationParameters } from "../types/query" +import type { UseMutationReturnType } from "../types/query" +import { useConfig as useZdConfig } from "./useConfig" + +export type UseSwitchChainParameters< + TZdConfig extends ZdConfig = ZdConfig, + context = unknown +> = Evaluate<{ + mutation?: + | UseMutationParameters< + SwitchChainData, + SwitchChainErrorType, + SwitchChainVariables< + TZdConfig, + TZdConfig["chains"][number]["id"] + >, + context + > + | undefined +}> + +export type UseSwitchChainReturnType< + TZdConfig extends ZdConfig = ZdConfig, + context = unknown +> = Evaluate< + UseMutationReturnType< + SwitchChainData, + SwitchChainErrorType, + SwitchChainVariables, + context + > & { + switchChain: SwitchChainMutate + switchChainAsync: SwitchChainMutateAsync + } +> + +export function useSwitchChain< + TZdConfig extends ZdConfig = ZdConfig, + context = unknown +>( + parameters: UseSwitchChainParameters = {} +): UseSwitchChainReturnType { + const { mutation } = parameters + const { setKernelAccountClient, setKernelAccount, setValidator } = + useSetKernelAccount() + const { validator, kernelAccountClient } = useKernelAccount() + + const zdConfig = useZdConfig() + const wagmiConfig = useWagmiConfig() + + const mutationOptions = switchChainMutationOptions( + zdConfig, + wagmiConfig, + validator, + kernelAccountClient + ) + const { mutate, mutateAsync, ...result } = useMutation({ + ...mutation, + ...mutationOptions, + onSuccess: (data, variables, context) => { + setKernelAccount(data.kernelAccount) + setKernelAccountClient(null) + setValidator(data.kernelValidator) + mutation?.onSuccess?.(data, variables, context) + } + }) + + type Return = UseSwitchChainReturnType + + return { + ...result, + switchChain: mutate as Return["switchChain"], + switchChainAsync: mutateAsync as Return["switchChainAsync"] + } +} diff --git a/src/index.ts b/src/index.ts index 1cd2c84..810816d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,3 +5,5 @@ export { ZeroDevProvider } from "./providers" export * from "./types" export * from "./utils" + +export { type Config, createConfig } from "./createConfig" diff --git a/src/providers/SessionContext.tsx b/src/providers/SessionContext.tsx index e9052ca..2947f46 100644 --- a/src/providers/SessionContext.tsx +++ b/src/providers/SessionContext.tsx @@ -15,6 +15,7 @@ import type { SessionType } from "../types" type UpdateSessionArgs = { sessionId: `0x${string}` + chainId: number smartAccount: `0x${string}` enableSignature: `0x${string}` sessionKey: `0x${string}` @@ -46,14 +47,17 @@ export function SessionProvider({ children }: SessionProviderProps) { function updateSession({ sessionId, + chainId, smartAccount, enableSignature, policies, sessionKey, permissions }: UpdateSessionArgs) { + const chainSessionID = + `${sessionId}:${chainId.toString()}` as `0x${string}` createSession( - sessionId, + chainSessionID, smartAccount, enableSignature, policies, @@ -62,7 +66,7 @@ export function SessionProvider({ children }: SessionProviderProps) { ) setSessions((prev) => ({ ...prev, - [sessionId]: { + [chainSessionID]: { smartAccount, enableSignature, policies, diff --git a/src/providers/SocialContext.tsx b/src/providers/SocialContext.tsx index 7a1f823..cdc0523 100644 --- a/src/providers/SocialContext.tsx +++ b/src/providers/SocialContext.tsx @@ -1,18 +1,12 @@ -import { - getSocialValidator, - initiateLogin, - isAuthorized -} from "@zerodev/social-validator" +import { initiateLogin } from "@zerodev/social-validator" import { createContext, useCallback, useContext, - useEffect, useMemo, useState } from "react" -import { useCreateKernelClientSocial } from "../hooks/useCreateKernelClientSocial" -import { useZeroDevConfig } from "./ZeroDevAppContext" +import { useConfig } from "../hooks/useConfig" interface SocialContextValue { isSocialPending: boolean @@ -34,21 +28,19 @@ export const SocialContext = createContext({ }) export function SocialProvider({ children }: SocialProviderProps) { - const { appId } = useZeroDevConfig() + const config = useConfig() + const projectId = config.projectIds[config.state.chainId] const [isSocialPending, setIsSocialPending] = useState(false) const login = useCallback( (socialProvider: "google" | "facebook", oauthCallbackUrl?: string) => { - if (!appId) { - throw new Error("missing appId") - } initiateLogin({ socialProvider, oauthCallbackUrl, - projectId: appId + projectId }) }, - [appId] + [projectId] ) return ( diff --git a/src/providers/ZeroDevAppContext.tsx b/src/providers/ZeroDevAppContext.tsx deleted file mode 100644 index 4d5d674..0000000 --- a/src/providers/ZeroDevAppContext.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { type ReactNode, createContext, useContext } from "react" -import { http, type Chain, type PublicClient, createPublicClient } from "viem" -import { ZERODEV_BUNDLER_URL } from "../utils" - -interface ZeroDevAppContextValue { - appId: string | null - chain: Chain | null - client: PublicClient | null -} - -export const ZeroDevAppContext = createContext({ - appId: null, - chain: null, - client: null -}) - -interface ZeroDevAppProviderProps { - children: ReactNode - appId: string | null - chain: Chain | null -} - -export function ZeroDevAppProvider({ - children, - appId, - chain -}: ZeroDevAppProviderProps) { - const client = - chain && - createPublicClient({ - chain: chain, - transport: http(`${ZERODEV_BUNDLER_URL}/${appId}`) - }) - return ( - - {children} - - ) -} - -export function useZeroDevConfig() { - const { appId, chain, client } = useContext(ZeroDevAppContext) - - return { appId, chain, client } -} diff --git a/src/providers/ZeroDevConfigContext.tsx b/src/providers/ZeroDevConfigContext.tsx new file mode 100644 index 0000000..c7e2df8 --- /dev/null +++ b/src/providers/ZeroDevConfigContext.tsx @@ -0,0 +1,30 @@ +import { type ReactNode, createContext, useContext } from "react" +import type { Config } from "../createConfig" + +interface ZeroDevAppContextValue { + config: Config | undefined +} + +export const ZeroDevConfigContext = createContext({ + config: undefined +}) + +interface ZeroDevConfigProviderProps { + config: Config +} + +export function ZeroDevConfigProvider( + parameters: React.PropsWithChildren +) { + const { children, config } = parameters + + return ( + + {children} + + ) +} diff --git a/src/providers/ZeroDevProvider.tsx b/src/providers/ZeroDevProvider.tsx index ab95575..c51050d 100644 --- a/src/providers/ZeroDevProvider.tsx +++ b/src/providers/ZeroDevProvider.tsx @@ -1,28 +1,23 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import type { ReactNode } from "react" -import type { Chain } from "wagmi/chains" +import type { Config } from "../createConfig" import { SessionProvider } from "./SessionContext" import { SocialProvider } from "./SocialContext" import { WalletConnectProvider } from "./WalletConnectProvider" -import { ZeroDevAppProvider } from "./ZeroDevAppContext" +import { ZeroDevConfigProvider } from "./ZeroDevConfigContext" import { ZeroDevValidatorProvider } from "./ZeroDevValidatorContext" export interface ZeroDevProviderProps { - appId: string | null - chain: Chain | null + config: Config children: ReactNode } -export function ZeroDevProvider({ - children, - appId, - chain -}: ZeroDevProviderProps) { +export function ZeroDevProvider({ children, config }: ZeroDevProviderProps) { const queryClient = new QueryClient() return ( - + @@ -30,7 +25,7 @@ export function ZeroDevProvider({ - + ) } diff --git a/src/providers/ZeroDevValidatorContext.tsx b/src/providers/ZeroDevValidatorContext.tsx index 1cc0991..ae19782 100644 --- a/src/providers/ZeroDevValidatorContext.tsx +++ b/src/providers/ZeroDevValidatorContext.tsx @@ -139,6 +139,7 @@ export type UseSetKernelAccountHook = { KernelSmartAccount > | null ) => void + disconnectClient: () => void } export type UseKernelAccountHook = { @@ -157,11 +158,19 @@ export function useSetKernelAccount(): UseSetKernelAccountHook { setKernelAccountClient } = useContext(ZeroDevValidatorContext) + const disconnectClient = () => { + setKernelAccount(null) + setKernelAccountClient(null) + setValidator(null) + setEntryPoint(null) + } + return { setKernelAccountClient, setKernelAccount, setEntryPoint, - setValidator + setValidator, + disconnectClient } } diff --git a/src/query/createBasicSession.ts b/src/query/createBasicSession.ts new file mode 100644 index 0000000..eb87541 --- /dev/null +++ b/src/query/createBasicSession.ts @@ -0,0 +1,49 @@ +import type { MutationOptions } from "@tanstack/query-core" +import type { KernelValidator } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import type { PublicClient } from "viem" +import { + type CreateBasicSessionErrorType, + type CreateBasicSessionParameters, + type CreateBasicSessionReturnType, + createBasicSession +} from "../actions/createBasicSession" +import type { Config } from "../createConfig" +import type { Mutate, MutateAsync } from "../types/query" + +export type CreateBasicSessionVariables = CreateBasicSessionParameters + +export type CreateBasicSessionData = CreateBasicSessionReturnType + +export type CreateBasicSessionMutate = Mutate< + CreateBasicSessionData, + CreateBasicSessionErrorType, + CreateBasicSessionVariables, + context +> + +export type CreateBasicSessionMutateAsync = MutateAsync< + CreateBasicSessionData, + CreateBasicSessionErrorType, + CreateBasicSessionVariables, + context +> + +export function createBasicSessionMutationOptions< + TEntryPoint extends EntryPoint +>( + entryPoint: TEntryPoint | null, + validator: KernelValidator | null, + config: Config +) { + return { + mutationFn(variables) { + return createBasicSession(entryPoint, validator, config, variables) + }, + mutationKey: ["createBasicSession"] + } as const satisfies MutationOptions< + CreateBasicSessionData, + CreateBasicSessionErrorType, + CreateBasicSessionVariables + > +} diff --git a/src/query/createKernelClientEOA.ts b/src/query/createKernelClientEOA.ts new file mode 100644 index 0000000..0898e3e --- /dev/null +++ b/src/query/createKernelClientEOA.ts @@ -0,0 +1,46 @@ +import type { MutationOptions } from "@tanstack/query-core" +import type { Config } from "wagmi" +import { + type CreateKernelClientEOAErrorType, + type CreateKernelClientEOAParameters, + type CreateKernelClientEOAReturnType, + createKernelClientEOA +} from "../actions/createKernelClientEOA" +import type { Config as ZdConfig } from "../createConfig" +import type { KernelVersionType } from "../types" +import type { Mutate, MutateAsync } from "../types/query" + +export type CreateKernelClientEOAVariables = CreateKernelClientEOAParameters + +export type CreateKernelClientEOAData = CreateKernelClientEOAReturnType + +export type CreateKernelClientEOAMutate = Mutate< + CreateKernelClientEOAData, + CreateKernelClientEOAErrorType, + CreateKernelClientEOAVariables, + context +> + +export type CreateKernelClientEOAMutateAsync = MutateAsync< + CreateKernelClientEOAData, + CreateKernelClientEOAErrorType, + CreateKernelClientEOAVariables, + context +> + +export function createKernelClientEOAMutationOptions( + config: Config, + zdConfig: ZdConfig, + version: KernelVersionType +) { + return { + mutationFn(variables) { + return createKernelClientEOA(config, zdConfig, version, variables) + }, + mutationKey: ["createKernelClientEOA", version] + } as const satisfies MutationOptions< + CreateKernelClientEOAData, + CreateKernelClientEOAErrorType, + CreateKernelClientEOAVariables + > +} diff --git a/src/query/createKernelClientPasskey.ts b/src/query/createKernelClientPasskey.ts new file mode 100644 index 0000000..e7206ec --- /dev/null +++ b/src/query/createKernelClientPasskey.ts @@ -0,0 +1,86 @@ +import type { MutationOptions } from "@tanstack/query-core" +import type { Evaluate } from "@wagmi/core/internal" +import { + type CreateKernelClientPasskeyErrorType, + type CreateKernelClientPasskeyParameters, + type CreateKernelClientPasskeyReturnType, + createKernelClientPasskey +} from "../actions/createKernelClientPasskey" +import type { Config } from "../createConfig" +import type { KernelVersionType } from "../types" +import type { Mutate, MutateAsync } from "../types/query" + +export type CreateKernelClientPasskeyVariables = + CreateKernelClientPasskeyParameters + +export type CreateKernelCLientPasskeyRegisterVariables = Omit< + CreateKernelClientPasskeyVariables, + "type" +> + +export type CreateKernelCLientPasskeyLoginVariables = Evaluate< + Omit | undefined +> + +export type CreateKernelClientPasskeyData = CreateKernelClientPasskeyReturnType + +export type CreateKernelClientPasskeyRegisterMutate = Mutate< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelCLientPasskeyRegisterVariables, + context +> + +export type CreateKernelClientPasskeyRegisterMutateAsync = + MutateAsync< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelCLientPasskeyRegisterVariables, + context + > + +export type CreateKernelClientPasskeyLoginMutate = Mutate< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelCLientPasskeyLoginVariables, + context +> + +export type CreateKernelClientPasskeyLoginMutateAsync = + MutateAsync< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelCLientPasskeyLoginVariables, + context + > + +export type CreateKernelClientPasskeyMutate = Mutate< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelClientPasskeyVariables, + context +> + +export type CreateKernelClientPasskeyMutateAsync = + MutateAsync< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelClientPasskeyVariables, + context + > + +export function createKernelClientPasskeyOptions( + config: Config, + version: KernelVersionType +) { + return { + mutationFn(variables) { + return createKernelClientPasskey(config, version, variables) + }, + mutationKey: ["createKernelClientPasskey", version] + } as const satisfies MutationOptions< + CreateKernelClientPasskeyData, + CreateKernelClientPasskeyErrorType, + CreateKernelClientPasskeyVariables + > +} diff --git a/src/query/createSession.ts b/src/query/createSession.ts new file mode 100644 index 0000000..4088a7d --- /dev/null +++ b/src/query/createSession.ts @@ -0,0 +1,47 @@ +import type { MutationOptions } from "@tanstack/query-core" +import type { KernelValidator } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import type { PublicClient } from "viem" +import { + type CreateSessionErrorType, + type CreateSessionParameters, + type CreateSessionReturnType, + createSession +} from "../actions/createSession" +import type { Config } from "../createConfig" +import type { Mutate, MutateAsync } from "../types/query" + +export type CreateSessionVariables = CreateSessionParameters + +export type CreateSessionData = CreateSessionReturnType + +export type CreateSessionMutate = Mutate< + CreateSessionData, + CreateSessionErrorType, + CreateSessionVariables, + context +> + +export type CreateSessionMutateAsync = MutateAsync< + CreateSessionData, + CreateSessionErrorType, + CreateSessionVariables, + context +> + +export function createSessionMutationOptions( + entryPoint: TEntryPoint | null, + validator: KernelValidator | null, + config: Config +) { + return { + mutationFn(variables) { + return createSession(entryPoint, validator, config, variables) + }, + mutationKey: ["createSession", validator] + } as const satisfies MutationOptions< + CreateSessionData, + CreateSessionErrorType, + CreateSessionVariables + > +} diff --git a/src/query/disconnectKernelClient.ts b/src/query/disconnectKernelClient.ts new file mode 100644 index 0000000..a220a2f --- /dev/null +++ b/src/query/disconnectKernelClient.ts @@ -0,0 +1,40 @@ +import type { Evaluate } from "@wagmi/core/internal" +import { + type DisconnectKernelClientErrorType, + type DisconnectKernelClientParameters, + type DisconnectKernelClientReturnType, + disconnectKernelClient +} from "../actions/disconnectKernelClient" +import type { Mutate, MutateAsync } from "../types/query" + +export type DisconnectKernelClientVariables = Evaluate< + DisconnectKernelClientParameters | undefined +> + +export type DisconnectKernelClientData = DisconnectKernelClientReturnType + +export type DisconnectKernelClientMutate = Mutate< + DisconnectKernelClientData, + DisconnectKernelClientErrorType, + DisconnectKernelClientVariables | undefined, + context +> + +export type DisconnectKernelClientMutateAsync = MutateAsync< + DisconnectKernelClientData, + DisconnectKernelClientErrorType, + DisconnectKernelClientVariables | undefined, + context +> + +export function disconnectKernelClientMutationOptions( + disconnectClient: () => void, + logoutSocial: () => Promise +) { + return { + mutationFn(variables = {}) { + return disconnectKernelClient(disconnectClient, logoutSocial) + }, + mutationKey: ["disconnectKernelClient"] + } as const +} diff --git a/src/query/getBalance.ts b/src/query/getBalance.ts new file mode 100644 index 0000000..bdd2dc7 --- /dev/null +++ b/src/query/getBalance.ts @@ -0,0 +1,50 @@ +import type { QueryOptions } from "@tanstack/query-core" +import type { Evaluate } from "@wagmi/core/internal" +import type { PublicClient } from "viem" +import { + type GetBalanceErrorType, + type GetBalanceParameters, + type GetBalanceReturnType, + getBalance +} from "../actions/getBalance" +import type { ScopeKeyParameter } from "../types" +import { filterQueryOptions } from "./utils" + +export type GetBalanceOptions = Evaluate< + Partial & ScopeKeyParameter +> + +export function getBalanceQueryOption( + publicClient: PublicClient | null, + options: GetBalanceOptions +) { + return { + async queryFn({ queryKey }) { + const { address, scopeKey: _, ...parameters } = queryKey[1] + if (!address) throw new Error("Address is required") + if (!publicClient) throw new Error("Public client is required") + + const balance = await getBalance(publicClient, { + ...(parameters as GetBalanceParameters), + address + }) + return balance ?? null + }, + queryKey: getBalanceQueryKey(options) + } as const satisfies QueryOptions< + GetBalanceQueryFnData, + GetBalanceErrorType, + GetBalanceData, + GetBalanceQueryKey + > +} + +export type GetBalanceQueryFnData = Evaluate + +export type GetBalanceData = GetBalanceQueryFnData + +export function getBalanceQueryKey(options: GetBalanceOptions) { + return ["balance", filterQueryOptions(options)] as const +} + +export type GetBalanceQueryKey = ReturnType diff --git a/src/query/getKernelClient.ts b/src/query/getKernelClient.ts new file mode 100644 index 0000000..f5eb0b8 --- /dev/null +++ b/src/query/getKernelClient.ts @@ -0,0 +1,77 @@ +import type { QueryOptions } from "@tanstack/query-core" +import type { Evaluate } from "@wagmi/core/internal" +import type { KernelAccountClient, KernelSmartAccount } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { + type GetKernelClientParameters, + getKernelClient +} from "../actions/getKernelClient" +import type { + GetKernelClientErrorType, + GetKernelClientReturnType +} from "../actions/getKernelClient" +import type { Config } from "../createConfig" +import type { ScopeKeyParameter } from "../types" +import { filterQueryOptions } from "./utils" + +export type GetKernelClientOptions = Evaluate< + Partial & ScopeKeyParameter +> + +export function getKernelClientQueryOption( + config: Config, + kernelAccount: KernelSmartAccount | null, + kernelAccountClient: KernelAccountClient | null, + entryPoint: EntryPoint | null, + chainId: number | null, + options: GetKernelClientOptions +) { + return { + async queryFn({ queryKey }) { + const { ...parameters } = queryKey[1] + const kernelClient = getKernelClient( + config, + kernelAccountClient, + kernelAccount, + { + ...(parameters as GetKernelClientParameters) + } + ) + return kernelClient ?? null + }, + queryKey: getKernelClientQueryKey( + kernelAccount, + kernelAccountClient, + chainId, + options + ) + } as const satisfies QueryOptions< + GetKernelClientQueryFnData, + GetKernelClientErrorType, + GetKernelClientData, + GetKernelClientQueryKey + > +} + +export type GetKernelClientQueryFnData = Evaluate + +export type GetKernelClientData = GetKernelClientQueryFnData + +export function getKernelClientQueryKey( + kernelAccount: KernelSmartAccount | null, + kernelAccountClient: KernelAccountClient | null, + chainId: number | null, + options: GetKernelClientOptions +) { + return [ + "kernelClient", + filterQueryOptions(options), + { + chainId, + kernelAccount, + kernelAccountClient + } + ] as const +} + +export type GetKernelClientQueryKey = ReturnType diff --git a/src/query/getSessionKernelClient.ts b/src/query/getSessionKernelClient.ts new file mode 100644 index 0000000..9c66991 --- /dev/null +++ b/src/query/getSessionKernelClient.ts @@ -0,0 +1,92 @@ +import type { QueryOptions } from "@tanstack/query-core" +import type { Evaluate } from "@wagmi/core/internal" +import type { KernelValidator } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import type { Address } from "viem" +import { + type GetSessionKernelClientErrorType, + type GetSessionKernelClientParameters, + type GetSessionKernelClientReturnType, + getSessionKernelClient +} from "../actions/getSessionKernelClient" +import type { Config } from "../createConfig" +import { KernelClientNotConnectedError } from "../errors" +import type { ScopeKeyParameter, SessionType } from "../types" +import { filterQueryOptions } from "./utils" + +export type GetSessionKernelClientOptions = Evaluate< + Partial & ScopeKeyParameter +> + +export function getSessionKernelClientQueryOption( + config: Config, + chainId: number | null, + validator: KernelValidator | null, + kernelAddress: Address | undefined, + entryPoint: EntryPoint | null, + session: SessionType | null, + options: GetSessionKernelClientOptions +) { + return { + async queryFn({ queryKey }) { + const { scopeKey, ...parameters } = queryKey[1] + if (!kernelAddress || !validator || !entryPoint) { + throw new KernelClientNotConnectedError() + } + const kernelClient = getSessionKernelClient( + config, + validator, + kernelAddress, + entryPoint, + session, + { + ...(parameters as GetSessionKernelClientParameters) + } + ) + return kernelClient ?? null + }, + queryKey: getSessionKernelClientQueryKey( + chainId, + validator, + kernelAddress, + entryPoint, + session, + options + ) + } as const satisfies QueryOptions< + GetSessionKernelClientQueryFnData, + GetSessionKernelClientErrorType, + GetSessionKernelClientData, + GetSessionKernelClientQueryKey + > +} + +export type GetSessionKernelClientQueryFnData = + Evaluate + +export type GetSessionKernelClientData = GetSessionKernelClientQueryFnData + +export function getSessionKernelClientQueryKey( + chainId: number | null, + validator: KernelValidator | null, + kernelAddress: Address | undefined, + entryPoint: EntryPoint | null, + session: SessionType | null, + options: GetSessionKernelClientOptions +) { + return [ + "sessionKernelClient", + filterQueryOptions(options), + { + chainId, + validator, + kernelAddress, + entryPoint, + session + } + ] as const +} + +export type GetSessionKernelClientQueryKey = ReturnType< + typeof getSessionKernelClientQueryKey +> diff --git a/src/query/sendTransaction.ts b/src/query/sendTransaction.ts new file mode 100644 index 0000000..1d746d1 --- /dev/null +++ b/src/query/sendTransaction.ts @@ -0,0 +1,69 @@ +import type { MutationOptions } from "@tanstack/query-core" +import type { KernelAccountClient } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { + type SendTransactionErrorType, + type SendTransactionParameters, + type SendTransactionReturnType, + sendTransaction +} from "../actions/sendTransaction" +import type { PaymasterERC20, PaymasterSPONSOR } from "../types" +import type { Mutate, MutateAsync } from "../types/query" + +export type SendTransactionVariables = SendTransactionParameters + +export type SendTransactionData = SendTransactionReturnType + +export type SendTransactionMutate = Mutate< + SendTransactionData, + SendTransactionErrorType, + SendTransactionVariables, + context +> + +export type SendTransactionMutateAsync = MutateAsync< + SendTransactionData, + SendTransactionErrorType, + SendTransactionVariables, + context +> + +export type SendTransactionType = + | "sendTransaction" + | "sendTransactionWithSession" + +export function createSendTransactionOptions( + type: SendTransactionType, + kernelClient: KernelAccountClient | undefined | null, + isParallel: boolean, + nonceKey: string | undefined, + chainId: number | null, + paymaster?: PaymasterERC20 | PaymasterSPONSOR, + sessionId?: `0x${string}` | null | undefined +) { + return { + mutationFn(variables) { + return sendTransaction( + kernelClient, + isParallel, + nonceKey, + variables + ) + }, + mutationKey: [ + type, + { + kernelClient, + isParallel, + nonceKey, + sessionId, + paymaster, + chainId + } + ] + } as const satisfies MutationOptions< + SendTransactionData, + SendTransactionErrorType, + SendTransactionVariables + > +} diff --git a/src/query/sendUserOperation.ts b/src/query/sendUserOperation.ts new file mode 100644 index 0000000..4a18705 --- /dev/null +++ b/src/query/sendUserOperation.ts @@ -0,0 +1,69 @@ +import type { MutationOptions } from "@tanstack/query-core" +import type { KernelAccountClient } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { + type SendUserOperationErrorType, + type SendUserOperationParameters, + type SendUserOperationReturnType, + sendUserOperation +} from "../actions/sendUserOperation" +import type { PaymasterERC20, PaymasterSPONSOR } from "../types" +import type { Mutate, MutateAsync } from "../types/query" + +export type SendUserOperationVariables = SendUserOperationParameters + +export type SendUserOperationData = SendUserOperationReturnType + +export type SendUserOperationMutate = Mutate< + SendUserOperationData, + SendUserOperationErrorType, + SendUserOperationVariables, + context +> + +export type SendUserOperationMutateAsync = MutateAsync< + SendUserOperationData, + SendUserOperationErrorType, + SendUserOperationVariables, + context +> + +export type SendUserOpType = + | "sendUserOperation" + | "sendUserOperationWithSession" + +export function createSendUserOperationOptions( + type: SendUserOpType, + kernelClient: KernelAccountClient | undefined | null, + isParallel: boolean, + nonceKey: string | undefined, + chainId: number | null, + paymaster?: PaymasterERC20 | PaymasterSPONSOR, + sessionId?: `0x${string}` | null | undefined +) { + return { + mutationFn(variables) { + return sendUserOperation( + kernelClient, + isParallel, + nonceKey, + variables + ) + }, + mutationKey: [ + type, + { + kernelClient, + isParallel, + nonceKey, + sessionId, + paymaster, + chainId + } + ] + } as const satisfies MutationOptions< + SendUserOperationData, + SendUserOperationErrorType, + SendUserOperationVariables + > +} diff --git a/src/query/setKernelClient.ts b/src/query/setKernelClient.ts new file mode 100644 index 0000000..01a24b6 --- /dev/null +++ b/src/query/setKernelClient.ts @@ -0,0 +1,45 @@ +import type { MutationOptions } from "@tanstack/query-core" +import type { KernelAccountClient } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import { + type SetKernelClientErrorType, + type SetKernelClientParameters, + type SetKernelClientReturnType, + setKernelClient +} from "../actions/setKernelClient" +import type { Mutate, MutateAsync } from "../types/query" + +export type SetKernelClientVariables = SetKernelClientParameters + +export type SetKernelClientData = SetKernelClientReturnType + +export type SetKernelClientMutate = Mutate< + SetKernelClientData, + SetKernelClientErrorType, + SetKernelClientVariables, + context +> + +export type SetKernelClientMutateAsync = MutateAsync< + SetKernelClientData, + SetKernelClientErrorType, + SetKernelClientVariables, + context +> + +export function setKernelClientMutationOptions( + setKernelAccountClient: ( + kernelAccountClient: KernelAccountClient + ) => void +) { + return { + mutationFn(variables) { + return setKernelClient(setKernelAccountClient, variables) + }, + mutationKey: ["setKernelClient"] + } as const satisfies MutationOptions< + SetKernelClientData, + SetKernelClientErrorType, + SetKernelClientVariables + > +} diff --git a/src/query/switchChain.ts b/src/query/switchChain.ts new file mode 100644 index 0000000..f42896a --- /dev/null +++ b/src/query/switchChain.ts @@ -0,0 +1,81 @@ +import type { MutateOptions, MutationOptions } from "@tanstack/query-core" +import type { Evaluate } from "@wagmi/core/internal" +import type { KernelAccountClient, KernelValidator } from "@zerodev/sdk" +import type { EntryPoint } from "permissionless/types" +import type { Config as WagmiConfig } from "wagmi" +import { + type SwitchChainErrorType, + type SwitchChainParameters, + type SwitchChainReturnType, + switchChain +} from "../actions/switchChain" +import type { Config as ZdConfig } from "../createConfig" + +export function switchChainMutationOptions( + zdConfig: TZdConfig, + wagmiConfig: WagmiConfig, + kernelValidator: KernelValidator | null, + kernelAccountClient: KernelAccountClient | null +) { + return { + mutationFn(variables) { + if (kernelAccountClient) { + throw new Error( + "SwitchChain not supported with self-defined kernel account client" + ) + } + return switchChain( + zdConfig, + wagmiConfig, + kernelValidator, + variables + ) + }, + mutationKey: ["switchChain"] + } as const satisfies MutationOptions< + SwitchChainData, + SwitchChainErrorType, + SwitchChainVariables + > +} + +export type SwitchChainData< + TZdConfig extends ZdConfig, + TChainId extends TZdConfig["chains"][number]["id"] +> = Evaluate> + +export type SwitchChainVariables< + TZdConfig extends ZdConfig, + TChainId extends TZdConfig["chains"][number]["id"] +> = Evaluate> + +export type SwitchChainMutate = < + TChainId extends TZdConfig["chains"][number]["id"] +>( + variables: SwitchChainVariables, + options?: Evaluate< + MutateOptions< + SwitchChainData, + SwitchChainErrorType, + Evaluate>, + context + > + > +) => void + +export type SwitchChainMutateAsync< + TZdConfig extends ZdConfig, + context = unknown +> = ( + variables: SwitchChainVariables, + options?: + | Evaluate< + MutateOptions< + SwitchChainData, + SwitchChainErrorType, + Evaluate>, + context + > + > + | undefined +) => Promise> diff --git a/src/query/utils.ts b/src/query/utils.ts new file mode 100644 index 0000000..c4ab67f --- /dev/null +++ b/src/query/utils.ts @@ -0,0 +1,24 @@ +export function filterQueryOptions>( + options: type +): type { + // destructuring is super fast + // biome-ignore format: no formatting + const { + // import('@tanstack/query-core').QueryOptions + _defaulted, behavior, gcTime, initialData, initialDataUpdatedAt, maxPages, meta, networkMode, queryFn, queryHash, queryKey, queryKeyHashFn, retry, retryDelay, structuralSharing, + + // import('@tanstack/query-core').InfiniteQueryObserverOptions + getPreviousPageParam, getNextPageParam, initialPageParam, + + // import('@tanstack/react-query').UseQueryOptions + _optimisticResults, enabled, notifyOnChangeProps, placeholderData, refetchInterval, refetchIntervalInBackground, refetchOnMount, refetchOnReconnect, refetchOnWindowFocus, retryOnMount, select, staleTime, suspense, throwOnError, + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // wagmi + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + config, connector, query, + ...rest + } = options + + return rest as type +} diff --git a/src/types/index.ts b/src/types/index.ts index 76b1df8..dbdb808 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -15,3 +15,8 @@ export { type GasTokenChainIdType, type GasTokenType } from "./paymaster" + +export { + type PartialBy, + type ScopeKeyParameter +} from "./utils" diff --git a/src/types/query.ts b/src/types/query.ts new file mode 100644 index 0000000..7b0e6e6 --- /dev/null +++ b/src/types/query.ts @@ -0,0 +1,160 @@ +import type { MutateOptions } from "@tanstack/query-core" +import { + type DefaultError, + type QueryKey, + type UseMutationOptions, + type UseMutationResult, + type UseQueryOptions, + type UseQueryResult, + useQuery as tanstack_useQuery +} from "@tanstack/react-query" +import type { Evaluate, UnionOmit } from "@wagmi/core/internal" +import { hashFn } from "@wagmi/core/query" +import type { ExactPartial } from "./utils" + +type MutateFn< + data = unknown, + error = unknown, + variables = void, + context = unknown +> = undefined extends variables + ? ( + variables?: variables, + options?: + | Evaluate> + | undefined + ) => Promise + : ( + variables: variables, + options?: + | Evaluate> + | undefined + ) => Promise + +export type Mutate< + data = unknown, + error = unknown, + variables = void, + context = unknown +> = ( + ...args: Parameters, context>> +) => void + +export type MutateAsync< + data = unknown, + error = unknown, + variables = void, + context = unknown +> = MutateFn, context> + +export type UseMutationParameters< + data = unknown, + error = Error, + variables = void, + context = unknown +> = Evaluate< + Omit< + UseMutationOptions, context>, + "mutationFn" | "mutationKey" | "throwOnError" + > +> + +export type UseMutationReturnType< + data = unknown, + error = Error, + variables = void, + context = unknown +> = Evaluate< + UnionOmit< + UseMutationResult, + "mutate" | "mutateAsync" + > +> + +export type UseQueryParameters< + queryFnData = unknown, + error = DefaultError, + data = queryFnData, + queryKey extends QueryKey = QueryKey +> = Evaluate< + ExactPartial< + Omit, "initialData"> + > & { + // Fix `initialData` type + initialData?: + | UseQueryOptions["initialData"] + | undefined + } +> + +export type QueryParameter< + queryFnData = unknown, + error = DefaultError, + data = queryFnData, + queryKey extends QueryKey = QueryKey +> = { + query?: + | Omit< + UseQueryParameters, + | "queryFn" + | "queryHash" + | "queryKey" + | "queryKeyHashFn" + | "throwOnError" + > + | undefined +} + +export type UseQueryReturnType = Evaluate< + UseQueryResult & { + queryKey: QueryKey + } +> + +export function useQuery( + parameters: UseQueryParameters & { + queryKey: QueryKey + } +): UseQueryReturnType { + const result = tanstack_useQuery({ + ...(parameters as any), + queryKeyHashFn: hashFn // for bigint support + }) as UseQueryReturnType + result.queryKey = parameters.queryKey + return result +} + +export function useQueryData< + queryFnData, + error, + data, + queryKey extends QueryKey +>( + parameters: UseQueryParameters & { + queryKey: QueryKey + } +): UseQueryDataReturnType { + const { data, ...rest } = tanstack_useQuery({ + ...(parameters as any), + queryKeyHashFn: hashFn // for bigint support + }) as UseQueryReturnType + + return { + ...rest, + queryKey: parameters.queryKey, + ...(data as data) + } as UseQueryDataReturnType +} + +export type UseQueryDataReturnType< + data = unknown, + error = DefaultError +> = Evaluate< + Omit< + UseQueryResult & { + queryKey: QueryKey + }, + "data" + > & + data +> diff --git a/src/types/utils.ts b/src/types/utils.ts new file mode 100644 index 0000000..616b59e --- /dev/null +++ b/src/types/utils.ts @@ -0,0 +1,17 @@ +/** Makes {@link key} optional in {@link type} while preserving type inference. */ +// s/o trpc (https://github.com/trpc/trpc/blob/main/packages/server/src/types.ts#L6) +export type PartialBy = ExactPartial< + Pick +> & + Omit + +/** + * Makes all properties of an object optional. + * + * Compatible with [`exactOptionalPropertyTypes`](https://www.typescriptlang.org/tsconfig#exactOptionalPropertyTypes). + */ +export type ExactPartial = { + [key in keyof type]?: type[key] | undefined +} + +export type ScopeKeyParameter = { scopeKey?: string | undefined } diff --git a/src/utils/config.ts b/src/utils/config.ts new file mode 100644 index 0000000..0385ebe --- /dev/null +++ b/src/utils/config.ts @@ -0,0 +1 @@ +export const version = "0.0.7" diff --git a/src/utils/config/createStorage.ts b/src/utils/config/createStorage.ts new file mode 100644 index 0000000..059fc48 --- /dev/null +++ b/src/utils/config/createStorage.ts @@ -0,0 +1,92 @@ +import type { Evaluate } from "@wagmi/core/internal" +import type { PartializedState } from "../../createConfig.js" +import { deserialize as deserialize_ } from "./deserialize.js" +import { serialize as serialize_ } from "./serialize.js" + +// key-values for loose autocomplete and typing +export type StorageItemMap = { + recentConnectorId: string + state: PartializedState +} + +export type Storage< + // biome-ignore lint/complexity/noBannedTypes: don't check + itemMap extends Record = {}, + storageItemMap extends StorageItemMap = StorageItemMap & itemMap +> = { + key: string + getItem< + key extends keyof storageItemMap, + value extends storageItemMap[key], + defaultValue extends value | null | undefined + >( + key: key, + defaultValue?: defaultValue | undefined + ): + | (defaultValue extends null ? value | null : value) + | Promise + setItem< + key extends keyof storageItemMap, + value extends storageItemMap[key] | null + >(key: key, value: value): void | Promise + removeItem(key: keyof storageItemMap): void | Promise +} + +export type BaseStorage = { + getItem( + key: string + ): string | null | undefined | Promise + setItem(key: string, value: string): void | Promise + removeItem(key: string): void | Promise +} + +export type CreateStorageParameters = { + deserialize?: ((value: string) => T) | undefined + key?: string | undefined + serialize?: ((value: T) => string) | undefined + storage?: Evaluate | undefined +} + +export function createStorage< + // biome-ignore lint/complexity/noBannedTypes: don't check + itemMap extends Record = {}, + storageItemMap extends StorageItemMap = StorageItemMap & itemMap +>(parameters: CreateStorageParameters): Evaluate> { + const { + deserialize = deserialize_, + key: prefix = "wagmi", + serialize = serialize_, + storage = noopStorage + } = parameters + + function unwrap(value: type): type | Promise { + if (value instanceof Promise) + return value.then((x) => x).catch(() => null) + return value + } + + return { + ...storage, + key: prefix, + async getItem(key, defaultValue) { + const value = storage.getItem(`${prefix}.${key as string}`) + const unwrapped = await unwrap(value) + if (unwrapped) return deserialize(unwrapped) ?? null + return (defaultValue ?? null) as any + }, + async setItem(key, value) { + const storageKey = `${prefix}.${key as string}` + if (value === null) await unwrap(storage.removeItem(storageKey)) + else await unwrap(storage.setItem(storageKey, serialize(value))) + }, + async removeItem(key) { + await unwrap(storage.removeItem(`${prefix}.${key as string}`)) + } + } +} + +export const noopStorage = { + getItem: () => null, + setItem: () => {}, + removeItem: () => {} +} satisfies BaseStorage diff --git a/src/utils/config/deserialize.ts b/src/utils/config/deserialize.ts new file mode 100644 index 0000000..9e7a53f --- /dev/null +++ b/src/utils/config/deserialize.ts @@ -0,0 +1,10 @@ +type Reviver = (key: string, value: any) => any + +export function deserialize(value: string, reviver?: Reviver): type { + return JSON.parse(value, (key, value_) => { + let value = value_ + if (value?.__type === "bigint") value = BigInt(value.value) + if (value?.__type === "Map") value = new Map(value.value) + return reviver?.(key, value) ?? value + }) +} diff --git a/src/utils/config/serialize.ts b/src/utils/config/serialize.ts new file mode 100644 index 0000000..2b3cdd8 --- /dev/null +++ b/src/utils/config/serialize.ts @@ -0,0 +1,116 @@ +/** + * Get the reference key for the circular value + * + * @param keys the keys to build the reference key from + * @param cutoff the maximum number of keys to include + * @returns the reference key + */ +function getReferenceKey(keys: string[], cutoff: number) { + return keys.slice(0, cutoff).join(".") || "." +} + +/** + * Faster `Array.prototype.indexOf` implementation build for slicing / splicing + * + * @param array the array to match the value in + * @param value the value to match + * @returns the matching index, or -1 + */ +function getCutoff(array: any[], value: any) { + const { length } = array + + for (let index = 0; index < length; ++index) { + if (array[index] === value) { + return index + 1 + } + } + + return 0 +} + +type StandardReplacer = (key: string, value: any) => any +type CircularReplacer = (key: string, value: any, referenceKey: string) => any + +/** + * Create a replacer method that handles circular values + * + * @param [replacer] a custom replacer to use for non-circular values + * @param [circularReplacer] a custom replacer to use for circular methods + * @returns the value to stringify + */ +function createReplacer( + replacer?: StandardReplacer | null | undefined, + circularReplacer?: CircularReplacer | null | undefined +): StandardReplacer { + const hasReplacer = typeof replacer === "function" + const hasCircularReplacer = typeof circularReplacer === "function" + + const cache: any[] = [] + const keys: string[] = [] + + return function replace(this: any, key: string, value: any) { + if (typeof value === "object") { + if (cache.length) { + const thisCutoff = getCutoff(cache, this) + + if (thisCutoff === 0) { + cache[cache.length] = this + } else { + cache.splice(thisCutoff) + keys.splice(thisCutoff) + } + + keys[keys.length] = key + + const valueCutoff = getCutoff(cache, value) + + if (valueCutoff !== 0) { + return hasCircularReplacer + ? circularReplacer.call( + this, + key, + value, + getReferenceKey(keys, valueCutoff) + ) + : `[ref=${getReferenceKey(keys, valueCutoff)}]` + } + } else { + cache[0] = value + keys[0] = key + } + } + + return hasReplacer ? replacer.call(this, key, value) : value + } +} + +/** + * Stringifier that handles circular values + * + * Forked from https://github.com/planttheidea/fast-stringify + * + * @param value to stringify + * @param [replacer] a custom replacer function for handling standard values + * @param [indent] the number of spaces to indent the output by + * @param [circularReplacer] a custom replacer function for handling circular values + * @returns the stringified output + */ +export function serialize( + value: any, + replacer?: StandardReplacer | null | undefined, + indent?: number | null | undefined, + circularReplacer?: CircularReplacer | null | undefined +) { + return JSON.stringify( + value, + createReplacer((key, value_) => { + let value = value_ + if (typeof value === "bigint") + value = { __type: "bigint", value: value_.toString() } + if (value instanceof Map) + value = { __type: "Map", value: Array.from(value_.entries()) } + return replacer?.(key, value) ?? value + }, circularReplacer), + indent ?? undefined + ) +} diff --git a/src/utils/deepEqual.ts b/src/utils/deepEqual.ts new file mode 100644 index 0000000..691bc1a --- /dev/null +++ b/src/utils/deepEqual.ts @@ -0,0 +1,45 @@ +/** Forked from https://github.com/epoberezkin/fast-deep-equal */ + +export function deepEqual(a: any, b: any) { + if (a === b) return true + + if (a && b && typeof a === "object" && typeof b === "object") { + if (a.constructor !== b.constructor) return false + + let length: number + let i: number + + if (Array.isArray(a) && Array.isArray(b)) { + length = a.length + if (length !== b.length) return false + for (i = length; i-- !== 0; ) + if (!deepEqual(a[i], b[i])) return false + return true + } + + if (a.valueOf !== Object.prototype.valueOf) + return a.valueOf() === b.valueOf() + if (a.toString !== Object.prototype.toString) + return a.toString() === b.toString() + + const keys = Object.keys(a) + length = keys.length + if (length !== Object.keys(b).length) return false + + for (i = length; i-- !== 0; ) + // biome-ignore lint/style/noNonNullAssertion: + if (!Object.prototype.hasOwnProperty.call(b, keys[i]!)) return false + + for (i = length; i-- !== 0; ) { + const key = keys[i] + + if (key && !deepEqual(a[key], b[key])) return false + } + + return true + } + + // true if both NaN, false otherwise + // biome-ignore lint/suspicious/noSelfCompare: + return a !== a && b !== b +} diff --git a/src/utils/webauthn.ts b/src/utils/webauthn.ts index ea403d1..bf579be 100644 --- a/src/utils/webauthn.ts +++ b/src/utils/webauthn.ts @@ -2,11 +2,13 @@ import { WEBAUTHN_VALIDATOR_ADDRESS_V06, WEBAUTHN_VALIDATOR_ADDRESS_V07 } from "@zerodev/passkey-validator" -import type { KernelVersionType } from "../types" +import { ENTRYPOINT_ADDRESS_V06 } from "permissionless" +import type { EntryPoint } from "permissionless/types" export const getWeb3AuthNValidatorFromVersion = ( - version: KernelVersionType + entryPoint: EntryPoint ): `0x${string}` => { - if (version === "v2") return WEBAUTHN_VALIDATOR_ADDRESS_V06 + if (entryPoint === ENTRYPOINT_ADDRESS_V06) + return WEBAUTHN_VALIDATOR_ADDRESS_V06 return WEBAUTHN_VALIDATOR_ADDRESS_V07 }