Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use wallet service's getCallsStatus rpc #755

Merged
merged 17 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 All @@ -102,34 +118,48 @@ async function jsonRpcRequest<TParams, TResult>(
method: string,
params: TParams,
url: string,
maxRetries: number = 3,
): Promise<TResult> {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
{
jsonrpc: "2.0",
id: "1",
method,
params,
},
bigIntReplacer,
),
});

if (!response.ok) {
throw new UserOpBuilderApiError(response.status, await response.text());
}

const data = await response.json();

if ("error" in data) {
throw new UserOpBuilderApiError(500, JSON.stringify(data.error));
let attempt = 0; // Initialize attempt counter
while (attempt < maxRetries) {
try {
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
{
jsonrpc: "2.0",
id: "1",
method,
params,
},
bigIntReplacer,
),
});

if (!response.ok) {
throw new UserOpBuilderApiError(response.status, await response.text());
}

const data = await response.json();

if ("error" in data) {
throw new UserOpBuilderApiError(500, JSON.stringify(data.error));
}

return data.result; // Return the result if successful
} catch (error) {
attempt++; // Increment the attempt counter
if (attempt >= maxRetries) {
throw error; // If max retries reached, throw the error
}
console.warn(`Attempt ${attempt} failed. Retrying...`); // Log the retry attempt
await new Promise((resolve) => setTimeout(resolve, 1000)); // Wait before retrying (optional delay)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have timeout duration in a const?

}
}

return data.result;
throw new Error("Failed to get a valid response after maximum retries");
}

export async function prepareCalls(
Expand All @@ -139,7 +169,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 +185,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