Skip to content

Commit

Permalink
Merge pull request #5171 from WalletConnect/feat/bundler-proxy
Browse files Browse the repository at this point in the history
feat: fetch call status via bundler url
  • Loading branch information
ganchoradkov authored Sep 18, 2024
2 parents ed0fae5 + fbb8fea commit b54b309
Show file tree
Hide file tree
Showing 4 changed files with 315 additions and 3 deletions.
2 changes: 2 additions & 0 deletions providers/universal-provider/src/constants/values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export const STORAGE = `${PROTOCOL}@${WC_VERSION}:${CONTEXT}:`;
export const RPC_URL = "https://rpc.walletconnect.com/v1/";

export const GENERIC_SUBPROVIDER_NAME = "generic";

export const BUNDLER_URL = `${RPC_URL}bundler`;
53 changes: 52 additions & 1 deletion providers/universal-provider/src/providers/eip155.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {

import { getChainId, getGlobal, getRpcUrl } from "../utils";
import EventEmitter from "events";
import { PROVIDER_EVENTS } from "../constants";
import { BUNDLER_URL, PROVIDER_EVENTS } from "../constants";
import { formatJsonRpcRequest } from "@walletconnect/jsonrpc-utils";

class Eip155Provider implements IProvider {
public name = "eip155";
Expand Down Expand Up @@ -45,6 +46,8 @@ class Eip155Provider implements IProvider {
return parseInt(this.getDefaultChain()) as unknown as T;
case "wallet_getCapabilities":
return (await this.getCapabilities(args)) as unknown as T;
case "wallet_getCallsStatus":
return (await this.getCallStatus(args)) as unknown as T;
default:
break;
}
Expand Down Expand Up @@ -198,6 +201,54 @@ class Eip155Provider implements IProvider {
}
return capabilities;
}

private async getCallStatus(args: RequestParams) {
const session = this.client.session.get(args.topic);
const bundlerName = session.sessionProperties?.bundler_name;
if (bundlerName) {
const bundlerUrl = this.getBundlerUrl(args.chainId, bundlerName);
try {
return await this.getUserOperationReceipt(bundlerUrl, args);
} catch (error) {
console.warn("Failed to fetch call status from bundler", error, bundlerUrl);
}
}
const customUrl = session.sessionProperties?.bundler_url;
if (customUrl) {
try {
return await this.getUserOperationReceipt(customUrl, args);
} catch (error) {
console.warn("Failed to fetch call status from custom bundler", error, customUrl);
}
}

if (this.namespace.methods.includes(args.request.method)) {
return await this.client.request(args as EngineTypes.RequestParams);
}

throw new Error("Fetching call status not approved by the wallet.");
}

private async getUserOperationReceipt(bundlerUrl: string, args: RequestParams) {
const url = new URL(bundlerUrl);
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
formatJsonRpcRequest("eth_getUserOperationReceipt", [args.request.params?.[0]]),
),
});
if (!response.ok) {
throw new Error(`Failed to fetch user operation receipt - ${response.status}`);
}
return await response.json();
}

private getBundlerUrl(cap2ChainId: string, bundlerName: string) {
return `${BUNDLER_URL}?projectId=${this.client.core.projectId}&chainId=${cap2ChainId}&bundler=${bundlerName}`;
}
}

export default Eip155Provider;
260 changes: 259 additions & 1 deletion providers/universal-provider/test/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
TEST_REQUIRED_NAMESPACES,
} from "./shared/constants";
import { getChainId, getGlobal, getRpcUrl, setGlobal } from "../src/utils";
import { RPC_URL } from "../src/constants";
import { BUNDLER_URL, RPC_URL } from "../src/constants";
import { formatJsonRpcResult } from "@walletconnect/jsonrpc-utils";
import { parseChainId } from "@walletconnect/utils";

Expand Down Expand Up @@ -510,6 +510,264 @@ describe("UniversalProvider", function () {
expect(provider.client.core.relayer.subscriber.subscriptions.size).to.eql(1);
});
});
describe("call status", () => {
it("should get call status request to wallet when bundler id is not provided", async () => {
const dapp = await UniversalProvider.init({
...TEST_PROVIDER_OPTS,
name: "dapp",
});
const wallet = await UniversalProvider.init({
...TEST_PROVIDER_OPTS,
name: "wallet",
});
const chains = ["eip155:1"];
await testConnectMethod(
{
dapp,
wallet,
},
{
requiredNamespaces: {
eip155: {
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
optionalNamespaces: {
eip155: {
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
namespaces: {
eip155: {
accounts: chains.map((chain) => `${chain}:${walletAddress}`),
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
},
);
const testResult = { result: "test result " };
await Promise.all([
new Promise<void>((resolve) => {
wallet.client.on("session_request", async (event) => {
expect(event.params.request.method).to.eql("wallet_getCallsStatus");
await wallet.client.respond({
topic: event.topic,
response: formatJsonRpcResult(event.id, testResult),
});
resolve();
});
}),
new Promise<void>(async (resolve) => {
const result = await dapp.request({
method: "wallet_getCallsStatus",
params: ["test params"],
});
expect(result).to.eql(testResult);
resolve();
}),
]);
});
it("should get call status request to bundler when custom bundler url is provided", async () => {
const dapp = await UniversalProvider.init({
...TEST_PROVIDER_OPTS,
name: "dapp",
});
const wallet = await UniversalProvider.init({
...TEST_PROVIDER_OPTS,
name: "wallet",
});
const chains = ["eip155:1"];
const customBundlerUrl = "https://custom-bundler.com";
await testConnectMethod(
{
dapp,
wallet,
},
{
requiredNamespaces: {
eip155: {
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
optionalNamespaces: {
eip155: {
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
namespaces: {
eip155: {
accounts: chains.map((chain) => `${chain}:${walletAddress}`),
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
sessionProperties: { bundler_url: customBundlerUrl },
},
);
const testResult = { result: "test result " };
// @ts-ignore
dapp.rpcProviders.eip155.getUserOperationReceipt = (bundlerUrl: string, args: any) => {
expect(bundlerUrl).to.eql(customBundlerUrl);
expect(args.request.method).to.eql("wallet_getCallsStatus");
return testResult;
};
await Promise.all([
new Promise<void>(async (resolve) => {
const result = await dapp.request({
method: "wallet_getCallsStatus",
params: ["test params"],
});
expect(result).to.eql(testResult);
resolve();
}),
]);
});
it("should get call status request to bundler when bundler name is provided", async () => {
const dapp = await UniversalProvider.init({
...TEST_PROVIDER_OPTS,
name: "dapp",
});
const wallet = await UniversalProvider.init({
...TEST_PROVIDER_OPTS,
name: "wallet",
});
const chains = ["eip155:1"];
await testConnectMethod(
{
dapp,
wallet,
},
{
requiredNamespaces: {
eip155: {
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
optionalNamespaces: {
eip155: {
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
namespaces: {
eip155: {
accounts: chains.map((chain) => `${chain}:${walletAddress}`),
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
sessionProperties: { bundler_name: "pimlico" },
},
);
const testResult = { result: "test result " };
// @ts-ignore
dapp.rpcProviders.eip155.getUserOperationReceipt = (bundlerUrl: string, args: any) => {
expect(bundlerUrl).to.include(BUNDLER_URL);
expect(args.request.method).to.eql("wallet_getCallsStatus");
return testResult;
};
await Promise.all([
new Promise<void>(async (resolve) => {
const result = await dapp.request({
method: "wallet_getCallsStatus",
params: ["test params"],
});
expect(result).to.eql(testResult);
resolve();
}),
]);
});
it("should get call status request to bundler and wallet when bundler url fails", async () => {
const dapp = await UniversalProvider.init({
...TEST_PROVIDER_OPTS,
name: "dapp",
});
const wallet = await UniversalProvider.init({
...TEST_PROVIDER_OPTS,
name: "wallet",
});
const chains = ["eip155:1"];
await testConnectMethod(
{
dapp,
wallet,
},
{
requiredNamespaces: {
eip155: {
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
optionalNamespaces: {
eip155: {
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
namespaces: {
eip155: {
accounts: chains.map((chain) => `${chain}:${walletAddress}`),
chains,
methods: ["wallet_getCallsStatus"],
events,
},
},
sessionProperties: { bundler_name: "pimlico" },
},
);
const testResult = { result: "test result " };
// @ts-ignore
dapp.rpcProviders.eip155.getUserOperationReceipt = (bundlerUrl: string, args: any) => {
throw new Error("Failed to fetch call status from bundler");
};
await Promise.all([
new Promise<void>((resolve) => {
wallet.client.on("session_request", async (event) => {
expect(event.params.request.method).to.eql("wallet_getCallsStatus");
await wallet.client.respond({
topic: event.topic,
response: formatJsonRpcResult(event.id, testResult),
});
resolve();
});
}),
new Promise<void>(async (resolve) => {
const result = await dapp.request({
method: "wallet_getCallsStatus",
params: ["test params"],
});
expect(result).to.eql(testResult);
resolve();
}),
]);
});
it("should receive rejection on get call status request when no bundler url or method is not approved", async () => {
await expect(
provider.request({
method: "wallet_getCallsStatus",
params: ["test params"],
}),
).rejects.toThrowError("Fetching call status not approved by the wallet.");
});
});
describe("caip validation", () => {
it("should reload after restart", async () => {
const dapp = await UniversalProvider.init({
Expand Down
3 changes: 2 additions & 1 deletion providers/universal-provider/test/shared/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface TestConnectParams {
relays?: RelayerTypes.ProtocolOptions[];
pairingTopic?: string;
qrCodeScanLatencyMs?: number;
sessionProperties?: SessionTypes.Struct["sessionProperties"];
}

export async function testConnectMethod(
Expand All @@ -39,9 +40,9 @@ export async function testConnectMethod(
relays: params?.relays || undefined,
pairingTopic: params?.pairingTopic || undefined,
};

const approveParams: Omit<EngineTypes.ApproveParams, "id"> = {
namespaces: params?.namespaces || TEST_NAMESPACES,
sessionProperties: params?.sessionProperties,
};

// We need to kick off the promise that binds the listener for `session_proposal` before `A.connect()`
Expand Down

0 comments on commit b54b309

Please sign in to comment.