Skip to content

Commit

Permalink
use wallet service's getCallsStatus rpc (#755)
Browse files Browse the repository at this point in the history
* add wallet_getCallsStatus on wallet service

* use wallet_getCallsStatus instead of direct pimlico bundler interaction

* add default timeout and interval

* chore: remove comments

* chore: run prettier

* fix userOperation reciept call

* set appkit light theme mode

* handle wallet_getCallsStatus on sample wallet

* add check for calls receipt status

* chore: remove console log

* use wallet service prod url

* added retry of max 3

Added max retry attempt of 3 on api call failure, bc some rpc calls(wallet_getCallsStatus) seems very flaky.

* changed DCA permissions from native-token-recurring-allowance to contract calls

* chors: run prettier

* removed retry, wallet_getCallsStatus got fixed.
  • Loading branch information
KannuSingh authored Nov 5, 2024
1 parent 6d43004 commit 2edb807
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ createAppKit({
features: {
analytics: true,
},
themeMode: "light",
termsConditionsUrl: "https://reown.com/terms-of-service",
privacyPolicyUrl: "https://reown.com/privacy-policy",
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ createAppKit({
features: {
analytics: true,
},
themeMode: "light",
termsConditionsUrl: "https://reown.com/terms-of-service",
privacyPolicyUrl: "https://reown.com/privacy-policy",
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand Down
18 changes: 13 additions & 5 deletions advanced/dapps/smart-sessions-demo/src/utils/DCAUtils.ts
Original file line number Diff line number Diff line change
@@ -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 },
Expand All @@ -21,6 +24,7 @@ export const intervalOptions = [
];

export function getSampleAsyncDCAPermissions(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
data: DCAFormSchemaType,
): Omit<
SmartSessionGrantPermissionsRequest,
Expand All @@ -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",
},
],
},
},
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -129,7 +145,7 @@ async function jsonRpcRequest<TParams, TResult>(
throw new UserOpBuilderApiError(500, JSON.stringify(data.error));
}

return data.result;
return data.result; // Return the result if successful
}

export async function prepareCalls(
Expand All @@ -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<PrepareCallsParams[], PrepareCallsReturnValue[]>(
Expand All @@ -156,11 +171,42 @@ 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<
SendPreparedCallsParams[],
SendPreparedCallsReturnValue[]
>("wallet_sendPreparedCalls", [args], url);
}

export async function getCallsStatus(
args: GetCallsStatusParams,
options: { timeout?: number; interval?: number } = {},
): Promise<GetCallsStatusReturnValue> {
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",
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<PrepareCallsReturnValue>
sendPreparedCalls(
Expand Down
66 changes: 60 additions & 6 deletions advanced/wallets/react-wallet-v2/src/pages/api/wallet.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -27,8 +33,7 @@ type JsonRpcResponse<T> = {
}
}

type SupportedMethods = 'wallet_prepareCalls' | 'wallet_sendPreparedCalls'

type SupportedMethods = 'wallet_prepareCalls' | 'wallet_sendPreparedCalls' | 'wallet_getCallsStatus'
const ERROR_CODES = {
INVALID_REQUEST: -32600,
METHOD_NOT_FOUND: -32601,
Expand Down Expand Up @@ -73,10 +78,52 @@ async function handleSendPreparedCalls(
return builder.sendPreparedCalls(projectId, data)
}

async function handleGetCallsStatus(
projectId: string,
params: GetCallsStatusParams[]
): Promise<GetCallsStatusReturnValue> {
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<PrepareCallsReturnValue[] | SendPreparedCallsReturnValue[] | ErrorResponse>
JsonRpcResponse<PrepareCallsReturnValue[] | SendPreparedCallsReturnValue[] | GetCallsStatusReturnValue[] | ErrorResponse>
>
) {
if (req.method === 'OPTIONS') {
Expand All @@ -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`))
Expand All @@ -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':
Expand All @@ -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}`)
}
Expand Down

0 comments on commit 2edb807

Please sign in to comment.