diff --git a/advanced/dapps/smart-sessions-demo/src/app/demo/dca/page.tsx b/advanced/dapps/smart-sessions-demo/src/app/demo/dca/page.tsx index fa1c39865..31c688ae2 100644 --- a/advanced/dapps/smart-sessions-demo/src/app/demo/dca/page.tsx +++ b/advanced/dapps/smart-sessions-demo/src/app/demo/dca/page.tsx @@ -28,6 +28,7 @@ createAppKit({ features: { analytics: true, }, + themeMode: "light", termsConditionsUrl: "https://reown.com/terms-of-service", privacyPolicyUrl: "https://reown.com/privacy-policy", }); diff --git a/advanced/dapps/smart-sessions-demo/src/app/demo/tictactoe/page.tsx b/advanced/dapps/smart-sessions-demo/src/app/demo/tictactoe/page.tsx index 4387f9f8b..f37595086 100644 --- a/advanced/dapps/smart-sessions-demo/src/app/demo/tictactoe/page.tsx +++ b/advanced/dapps/smart-sessions-demo/src/app/demo/tictactoe/page.tsx @@ -28,6 +28,7 @@ createAppKit({ features: { analytics: true, }, + themeMode: "light", termsConditionsUrl: "https://reown.com/terms-of-service", privacyPolicyUrl: "https://reown.com/privacy-policy", }); diff --git a/advanced/dapps/smart-sessions-demo/src/components/TicTacToeComponents/TicTacToeBoard.tsx b/advanced/dapps/smart-sessions-demo/src/components/TicTacToeComponents/TicTacToeBoard.tsx index 54ed44ae0..55c176296 100644 --- a/advanced/dapps/smart-sessions-demo/src/components/TicTacToeComponents/TicTacToeBoard.tsx +++ b/advanced/dapps/smart-sessions-demo/src/components/TicTacToeComponents/TicTacToeBoard.tsx @@ -8,16 +8,12 @@ import { Loader2 } from "lucide-react"; import { toast } from "sonner"; import DisplayPlayerScore from "./DisplayPlayerScore"; import PositionSquare from "./PositionSquare"; -import { createPimlicoBundlerClient } from "permissionless/clients/pimlico"; -import { baseSepolia } from "viem/chains"; -import { ENTRYPOINT_ADDRESS_V07 } from "permissionless"; -import { http } from "viem"; import { checkWinner, getBoardState, transformBoard, } from "@/utils/TicTacToeUtils"; -import { getBundlerUrl } from "@/utils/ConstantsUtil"; +import { getCallsStatus } from "@/utils/UserOpBuilderServiceUtils"; function TicTacToeBoard() { const { smartSession, setSmartSession } = useTicTacToeContext(); @@ -39,17 +35,8 @@ function TicTacToeBoard() { try { const data = await handleUserMove(gameId, position); const { userOpIdentifier } = data; - const bundlerClient = createPimlicoBundlerClient({ - chain: baseSepolia, - entryPoint: ENTRYPOINT_ADDRESS_V07, - transport: http(getBundlerUrl(), { - timeout: 300000, - }), - }); + await getCallsStatus(userOpIdentifier); - await bundlerClient.waitForUserOperationReceipt({ - hash: userOpIdentifier, - }); console.log("User move made successfully"); // After the user's move, read the updated board state const updatedBoard = await getBoardState(gameId); diff --git a/advanced/dapps/smart-sessions-demo/src/utils/ConstantsUtil.ts b/advanced/dapps/smart-sessions-demo/src/utils/ConstantsUtil.ts index 3326a860d..fe7e2e57d 100644 --- a/advanced/dapps/smart-sessions-demo/src/utils/ConstantsUtil.ts +++ b/advanced/dapps/smart-sessions-demo/src/utils/ConstantsUtil.ts @@ -15,7 +15,7 @@ export function getPublicUrl() { export const CUSTOM_WALLET = "wc:custom_wallet"; export const USEROP_BUILDER_SERVICE_BASE_URL = - "https://react-wallet.walletconnect.com/api/wallet"; + "https://rpc.walletconnect.org/v1/wallet"; let storedCustomWallet; if (typeof window !== "undefined") { diff --git a/advanced/dapps/smart-sessions-demo/src/utils/DCAUtils.ts b/advanced/dapps/smart-sessions-demo/src/utils/DCAUtils.ts index 2dd80304e..7cc6ea4fd 100644 --- a/advanced/dapps/smart-sessions-demo/src/utils/DCAUtils.ts +++ b/advanced/dapps/smart-sessions-demo/src/utils/DCAUtils.ts @@ -1,6 +1,9 @@ import { DCAFormSchemaType } from "@/schema/DCAFormSchema"; import { SmartSessionGrantPermissionsRequest } from "@reown/appkit-experimental/smart-session"; -import { parseEther, toHex } from "viem"; +import { + abi as donutContractAbi, + address as donutContractAddress, +} from "./DonutContract"; export const assetsToAllocate = [ { value: "eth", label: "ETH", supported: true }, @@ -21,6 +24,7 @@ export const intervalOptions = [ ]; export function getSampleAsyncDCAPermissions( + // eslint-disable-next-line @typescript-eslint/no-unused-vars data: DCAFormSchemaType, ): Omit< SmartSessionGrantPermissionsRequest, @@ -29,11 +33,15 @@ export function getSampleAsyncDCAPermissions( return { permissions: [ { - type: "native-token-recurring-allowance", + type: "contract-call", data: { - allowance: toHex(parseEther(data.allocationAmount.toString())), - period: calculateInterval(data.investmentInterval, data.intervalUnit), - start: Date.now(), + address: donutContractAddress, + abi: donutContractAbi, + functions: [ + { + functionName: "purchase", + }, + ], }, }, ], diff --git a/advanced/dapps/smart-sessions-demo/src/utils/UserOpBuilderServiceUtils.ts b/advanced/dapps/smart-sessions-demo/src/utils/UserOpBuilderServiceUtils.ts index f6d1027eb..07eaeb216 100644 --- a/advanced/dapps/smart-sessions-demo/src/utils/UserOpBuilderServiceUtils.ts +++ b/advanced/dapps/smart-sessions-demo/src/utils/UserOpBuilderServiceUtils.ts @@ -86,6 +86,22 @@ export type SendPreparedCallsParams = { }; export type SendPreparedCallsReturnValue = string; +export type GetCallsStatusParams = string; +export type GetCallsStatusReturnValue = { + status: "PENDING" | "CONFIRMED"; + receipts?: { + logs: { + address: `0x${string}`; + data: `0x${string}`; + topics: `0x${string}`[]; + }[]; + status: `0x${string}`; // Hex 1 or 0 for success or failure, respectively + blockHash: `0x${string}`; + blockNumber: `0x${string}`; + gasUsed: `0x${string}`; + transactionHash: `0x${string}`; + }[]; +}; // Define a custom error type export class UserOpBuilderApiError extends Error { @@ -129,7 +145,7 @@ async function jsonRpcRequest( throw new UserOpBuilderApiError(500, JSON.stringify(data.error)); } - return data.result; + return data.result; // Return the result if successful } export async function prepareCalls( @@ -139,7 +155,6 @@ export async function prepareCalls( if (!projectId) { throw new Error("NEXT_PUBLIC_PROJECT_ID is not set"); } - const url = `${USEROP_BUILDER_SERVICE_BASE_URL}?projectId=${projectId}`; return jsonRpcRequest( @@ -156,7 +171,6 @@ export async function sendPreparedCalls( if (!projectId) { throw new Error("NEXT_PUBLIC_PROJECT_ID is not set"); } - const url = `${USEROP_BUILDER_SERVICE_BASE_URL}?projectId=${projectId}`; return jsonRpcRequest< @@ -164,3 +178,35 @@ export async function sendPreparedCalls( SendPreparedCallsReturnValue[] >("wallet_sendPreparedCalls", [args], url); } + +export async function getCallsStatus( + args: GetCallsStatusParams, + options: { timeout?: number; interval?: number } = {}, +): Promise { + const projectId = process.env["NEXT_PUBLIC_PROJECT_ID"]; + if (!projectId) { + throw new Error("NEXT_PUBLIC_PROJECT_ID is not set"); + } + + const url = `${USEROP_BUILDER_SERVICE_BASE_URL}?projectId=${projectId}`; + + const { timeout = 30000, interval = 3000 } = options; // Default timeout to 30 seconds and interval to 2 second + const endTime = Date.now() + timeout; + while (Date.now() < endTime) { + const response = await jsonRpcRequest< + GetCallsStatusParams[], + GetCallsStatusReturnValue + >("wallet_getCallsStatus", [args], url); + + // Check if the response is valid (not null) + if (response.status === "CONFIRMED") { + return response; + } + + // Wait for the specified interval before polling again + await new Promise((resolve) => setTimeout(resolve, interval)); + } + throw new Error( + "Timeout: No valid response received from wallet_getCallsStatus", + ); +} diff --git a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts index c90e0d7a2..31149746b 100644 --- a/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts +++ b/advanced/wallets/react-wallet-v2/src/lib/smart-accounts/builders/UserOpBuilder.ts @@ -58,6 +58,23 @@ export type SendPreparedCallsParams = { } export type SendPreparedCallsReturnValue = string + +export type GetCallsStatusParams = string; +export type GetCallsStatusReturnValue = { + status: 'PENDING' | 'CONFIRMED' + receipts?: { + logs: { + address: `0x${string}` + data: `0x${string}` + topics: `0x${string}`[] + }[] + status: `0x${string}` // Hex 1 or 0 for success or failure, respectively + blockHash: `0x${string}` + blockNumber: `0x${string}` + gasUsed: `0x${string}` + transactionHash: `0x${string}` + }[] +} export interface UserOpBuilder { prepareCalls(projectId: string, params: PrepareCallsParams): Promise sendPreparedCalls( diff --git a/advanced/wallets/react-wallet-v2/src/pages/api/wallet.ts b/advanced/wallets/react-wallet-v2/src/pages/api/wallet.ts index 27936f07f..a45cf5e76 100644 --- a/advanced/wallets/react-wallet-v2/src/pages/api/wallet.ts +++ b/advanced/wallets/react-wallet-v2/src/pages/api/wallet.ts @@ -1,13 +1,19 @@ import { ErrorResponse, + GetCallsStatusParams, + GetCallsStatusReturnValue, PrepareCallsParams, PrepareCallsReturnValue, SendPreparedCallsParams, SendPreparedCallsReturnValue } from '@/lib/smart-accounts/builders/UserOpBuilder' import { getChainById } from '@/utils/ChainUtil' +import { PIMLICO_NETWORK_NAMES } from '@/utils/SmartAccountUtil' import { getUserOpBuilder } from '@/utils/UserOpBuilderUtil' import { NextApiRequest, NextApiResponse } from 'next' +import { ENTRYPOINT_ADDRESS_V07 } from 'permissionless' +import { createPimlicoBundlerClient } from 'permissionless/clients/pimlico' +import { http, toHex } from 'viem' type JsonRpcRequest = { jsonrpc: '2.0' @@ -27,8 +33,7 @@ type JsonRpcResponse = { } } -type SupportedMethods = 'wallet_prepareCalls' | 'wallet_sendPreparedCalls' - +type SupportedMethods = 'wallet_prepareCalls' | 'wallet_sendPreparedCalls' | 'wallet_getCallsStatus' const ERROR_CODES = { INVALID_REQUEST: -32600, METHOD_NOT_FOUND: -32601, @@ -73,10 +78,52 @@ async function handleSendPreparedCalls( return builder.sendPreparedCalls(projectId, data) } +async function handleGetCallsStatus( + projectId: string, + params: GetCallsStatusParams[] +): Promise { + const [userOpIdentifier] = params + const chainId = userOpIdentifier.split(':')[0] + const userOpHash = userOpIdentifier.split(':')[1] + const chain = getChainById(parseInt(chainId, 16)) + const pimlicoChainName = PIMLICO_NETWORK_NAMES[chain.name] + const apiKey = process.env.NEXT_PUBLIC_PIMLICO_KEY + const localBundlerUrl = process.env.NEXT_PUBLIC_LOCAL_BUNDLER_URL + const bundlerUrl = localBundlerUrl || `https://api.pimlico.io/v1/${pimlicoChainName}/rpc?apikey=${apiKey}` + const bundlerClient = createPimlicoBundlerClient({ + entryPoint: ENTRYPOINT_ADDRESS_V07, + transport: http(bundlerUrl) + }) + const userOpReceipt = await bundlerClient.getUserOperationReceipt({ + hash: userOpHash as `0x${string}` + }) + const receipt: GetCallsStatusReturnValue = { + status: userOpReceipt ? 'CONFIRMED' : 'PENDING', + receipts: userOpReceipt + ? [ + { + logs: userOpReceipt.logs.map(log => ({ + data: log.data, + address: log.address, + topics: log.topics + })), + blockHash: userOpReceipt.receipt.blockHash, + blockNumber: toHex(userOpReceipt.receipt.blockNumber), + gasUsed: toHex(userOpReceipt.actualGasUsed), + transactionHash: userOpReceipt.receipt.transactionHash, + status: userOpReceipt.success ? '0x1' : '0x0' + } + ] + : undefined + } + return receipt + +} + export default async function handler( req: NextApiRequest, res: NextApiResponse< - JsonRpcResponse + JsonRpcResponse > ) { if (req.method === 'OPTIONS') { @@ -91,8 +138,7 @@ export default async function handler( const jsonRpcRequest: JsonRpcRequest = req.body const { id, method, params } = jsonRpcRequest - - if (!['wallet_prepareCalls', 'wallet_sendPreparedCalls'].includes(method)) { + if (!['wallet_prepareCalls', 'wallet_sendPreparedCalls', 'wallet_getCallsStatus'].includes(method)) { return res .status(200) .json(createErrorResponse(id, ERROR_CODES.METHOD_NOT_FOUND, `${method} method not found`)) @@ -106,7 +152,7 @@ export default async function handler( } try { - let response: PrepareCallsReturnValue | SendPreparedCallsReturnValue + let response: PrepareCallsReturnValue | SendPreparedCallsReturnValue | GetCallsStatusReturnValue switch (method as SupportedMethods) { case 'wallet_prepareCalls': @@ -125,6 +171,14 @@ export default async function handler( result: [response] as SendPreparedCallsReturnValue[] }) + case 'wallet_getCallsStatus': + response = await handleGetCallsStatus(projectId, params as GetCallsStatusParams[]) + return res.status(200).json({ + jsonrpc: '2.0', + id, + result: [response] as GetCallsStatusReturnValue[] + }) + default: throw new Error(`Unsupported method: ${method}`) }