From c0f141f1ade265593406b8b5790ef12ba70e3e8a Mon Sep 17 00:00:00 2001 From: omerbek <21971506+omerbek@users.noreply.github.com> Date: Thu, 3 Sep 2026 10:11:06 +0300 Subject: [PATCH 1/2] fix: keep wallet connect retryable after rejection (#6) --- app-kit-bridge-evm/package.json | 4 +- app-kit-bridge-evm/src/connect.test.ts | 55 ++++++++++++++++ app-kit-bridge-evm/src/connect.ts | 35 +++++++++++ app-kit-bridge-evm/src/main.ts | 22 +++---- app-kit-bridge-solana/package.json | 4 +- app-kit-bridge-solana/src/connect.test.ts | 77 +++++++++++++++++++++++ app-kit-bridge-solana/src/connect.ts | 46 ++++++++++++++ app-kit-bridge-solana/src/main.ts | 43 ++++++------- app-kit-send/package.json | 4 +- app-kit-send/src/connect.test.ts | 55 ++++++++++++++++ app-kit-send/src/connect.ts | 35 +++++++++++ app-kit-send/src/main.ts | 22 +++---- app-kit-swap/package.json | 4 +- app-kit-swap/src/connect.test.ts | 55 ++++++++++++++++ app-kit-swap/src/connect.ts | 35 +++++++++++ app-kit-swap/src/main.ts | 22 +++---- 16 files changed, 449 insertions(+), 69 deletions(-) create mode 100644 app-kit-bridge-evm/src/connect.test.ts create mode 100644 app-kit-bridge-evm/src/connect.ts create mode 100644 app-kit-bridge-solana/src/connect.test.ts create mode 100644 app-kit-bridge-solana/src/connect.ts create mode 100644 app-kit-send/src/connect.test.ts create mode 100644 app-kit-send/src/connect.ts create mode 100644 app-kit-swap/src/connect.test.ts create mode 100644 app-kit-swap/src/connect.ts diff --git a/app-kit-bridge-evm/package.json b/app-kit-bridge-evm/package.json index c068cbe..abd824b 100644 --- a/app-kit-bridge-evm/package.json +++ b/app-kit-bridge-evm/package.json @@ -5,11 +5,13 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "test": "vitest run", "preview": "vite preview" }, "devDependencies": { "typescript": "~7.0.2", - "vite": "^8.2.1" + "vite": "^8.2.1", + "vitest": "^4.1.9" }, "dependencies": { "@circle-fin/app-kit": "^1.12.0", diff --git a/app-kit-bridge-evm/src/connect.test.ts b/app-kit-bridge-evm/src/connect.test.ts new file mode 100644 index 0000000..777666e --- /dev/null +++ b/app-kit-bridge-evm/src/connect.test.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, vi } from "vitest"; +import { connectEvmProvider } from "./connect.ts"; +import type { BrowserWalletProvider } from "./connect.ts"; + +describe("connectEvmProvider", () => { + it("returns the provider only after permission and account lookup succeed", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(["0x3333333333333333333333333333333333333333"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x3333333333333333333333333333333333333333", + }); + expect(provider.request).toHaveBeenNthCalledWith(1, { + method: "eth_requestAccounts", + params: undefined, + }); + expect(provider.request).toHaveBeenNthCalledWith(2, { + method: "eth_accounts", + params: undefined, + }); + }); + + it("does not return a connected provider when permission is rejected", async () => { + const rejection = new Error("User rejected the request"); + const provider = { + request: vi.fn().mockRejectedValue(rejection), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toBe(rejection); + expect(provider.request).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app-kit-bridge-evm/src/connect.ts b/app-kit-bridge-evm/src/connect.ts new file mode 100644 index 0000000..97e653f --- /dev/null +++ b/app-kit-bridge-evm/src/connect.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CreateViemAdapterFromProviderParams } from "@circle-fin/adapter-viem-v2"; + +export type BrowserWalletProvider = + CreateViemAdapterFromProviderParams["provider"]; + +export async function connectEvmProvider(provider: BrowserWalletProvider) { + await provider.request({ + method: "eth_requestAccounts", + params: undefined, + }); + const accounts = (await provider.request({ + method: "eth_accounts", + params: undefined, + })) as string[]; + + return { provider, account: accounts[0] }; +} diff --git a/app-kit-bridge-evm/src/main.ts b/app-kit-bridge-evm/src/main.ts index d5b2fa8..e493142 100644 --- a/app-kit-bridge-evm/src/main.ts +++ b/app-kit-bridge-evm/src/main.ts @@ -18,9 +18,8 @@ import { AppKit } from "@circle-fin/app-kit"; import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2"; -import type { CreateViemAdapterFromProviderParams } from "@circle-fin/adapter-viem-v2"; - -type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"]; +import { connectEvmProvider } from "./connect.ts"; +import type { BrowserWalletProvider } from "./connect.ts"; type EIP6963ProviderDetail = { info: { @@ -71,19 +70,14 @@ async function handleEvmConnect() { try { connectEvmButton.disabled = true; - evmProvider = await getProvider(); - await evmProvider.request({ - method: "eth_requestAccounts", - params: undefined, - }); - const accounts = (await evmProvider.request({ - method: "eth_accounts", - params: undefined, - })) as string[]; + const connection = await connectEvmProvider(await getProvider()); + evmProvider = connection.provider; - walletInfo.textContent = accounts[0] ?? "Connected"; - bridgeButton.disabled = !evmProvider; + walletInfo.textContent = connection.account ?? "Connected"; + bridgeButton.disabled = false; } catch (error) { + evmProvider = null; + bridgeButton.disabled = true; render({ error: error instanceof Error ? error.message : "Unknown error" }); } finally { connectEvmButton.disabled = Boolean(evmProvider); diff --git a/app-kit-bridge-solana/package.json b/app-kit-bridge-solana/package.json index 617ae3c..e3122bc 100644 --- a/app-kit-bridge-solana/package.json +++ b/app-kit-bridge-solana/package.json @@ -5,11 +5,13 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "test": "vitest run", "preview": "vite preview" }, "devDependencies": { "typescript": "~7.0.2", - "vite": "^8.2.1" + "vite": "^8.2.1", + "vitest": "^4.1.9" }, "dependencies": { "@circle-fin/adapter-solana": "^1.6.5", diff --git a/app-kit-bridge-solana/src/connect.test.ts b/app-kit-bridge-solana/src/connect.test.ts new file mode 100644 index 0000000..db61cc9 --- /dev/null +++ b/app-kit-bridge-solana/src/connect.test.ts @@ -0,0 +1,77 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, vi } from "vitest"; +import { + connectEvmProvider, + connectSolanaProvider, +} from "./connect.ts"; +import type { + BrowserWalletProvider, + SolanaWalletProvider, +} from "./connect.ts"; + +describe("wallet connection", () => { + it("returns the EVM provider only after account lookup succeeds", async () => { + const account = "0x4444444444444444444444444444444444444444"; + const provider = { + request: vi + .fn() + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce([account]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account, + }); + }); + + it("does not return an EVM provider when permission is rejected", async () => { + const rejection = new Error("User rejected the request"); + const provider = { + request: vi.fn().mockRejectedValue(rejection), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toBe(rejection); + expect(provider.request).toHaveBeenCalledTimes(1); + }); + + it("does not return a Solana provider when connection is rejected", async () => { + const rejection = new Error("User rejected the request"); + const provider = { + connect: vi.fn().mockRejectedValue(rejection), + } as unknown as SolanaWalletProvider; + + await expect(connectSolanaProvider(provider)).rejects.toBe(rejection); + expect(provider.connect).toHaveBeenCalledTimes(1); + }); + + it("returns the Solana provider only after connection succeeds", async () => { + const publicKey = { toString: () => "solana-address" }; + const provider = { + connect: vi.fn().mockResolvedValue({ publicKey }), + publicKey: null, + } as unknown as SolanaWalletProvider; + + await expect(connectSolanaProvider(provider)).resolves.toEqual({ + provider, + address: "solana-address", + }); + }); +}); diff --git a/app-kit-bridge-solana/src/connect.ts b/app-kit-bridge-solana/src/connect.ts new file mode 100644 index 0000000..d58d7f1 --- /dev/null +++ b/app-kit-bridge-solana/src/connect.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CreateSolanaAdapterFromProviderParams } from "@circle-fin/adapter-solana"; +import type { CreateViemAdapterFromProviderParams } from "@circle-fin/adapter-viem-v2"; + +export type BrowserWalletProvider = + CreateViemAdapterFromProviderParams["provider"]; +export type SolanaWalletProvider = + CreateSolanaAdapterFromProviderParams["provider"]; + +export async function connectEvmProvider(provider: BrowserWalletProvider) { + await provider.request({ + method: "eth_requestAccounts", + params: undefined, + }); + const accounts = (await provider.request({ + method: "eth_accounts", + params: undefined, + })) as string[]; + + return { provider, account: accounts[0] }; +} + +export async function connectSolanaProvider(provider: SolanaWalletProvider) { + const connection = await provider.connect(); + const address = + connection.publicKey?.toString() ?? provider.publicKey?.toString(); + + return { provider, address }; +} diff --git a/app-kit-bridge-solana/src/main.ts b/app-kit-bridge-solana/src/main.ts index 95e755b..d645796 100644 --- a/app-kit-bridge-solana/src/main.ts +++ b/app-kit-bridge-solana/src/main.ts @@ -19,10 +19,14 @@ import { AppKit } from "@circle-fin/app-kit"; import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2"; import { createSolanaAdapterFromProvider } from "@circle-fin/adapter-solana"; -import type { CreateViemAdapterFromProviderParams } from "@circle-fin/adapter-viem-v2"; -import type { CreateSolanaAdapterFromProviderParams } from "@circle-fin/adapter-solana"; - -type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"]; +import { + connectEvmProvider, + connectSolanaProvider, +} from "./connect.ts"; +import type { + BrowserWalletProvider, + SolanaWalletProvider, +} from "./connect.ts"; type EIP6963ProviderDetail = { info: { @@ -34,8 +38,6 @@ type EIP6963ProviderDetail = { provider: BrowserWalletProvider; }; -type SolanaWalletProvider = CreateSolanaAdapterFromProviderParams["provider"]; - declare global { interface WindowEventMap { "eip6963:announceProvider": CustomEvent; @@ -79,19 +81,14 @@ async function handleEvmConnect() { try { connectEvmButton.disabled = true; - evmProvider = await getProvider(); - await evmProvider.request({ - method: "eth_requestAccounts", - params: undefined, - }); - const accounts = (await evmProvider.request({ - method: "eth_accounts", - params: undefined, - })) as string[]; + const connection = await connectEvmProvider(await getProvider()); + evmProvider = connection.provider; - walletInfo.textContent = accounts[0] ?? "Connected"; - bridgeButton.disabled = !evmProvider || !solanaProvider; + walletInfo.textContent = connection.account ?? "Connected"; + bridgeButton.disabled = !solanaProvider; } catch (error) { + evmProvider = null; + bridgeButton.disabled = true; render({ error: error instanceof Error ? error.message : "Unknown error" }); } finally { connectEvmButton.disabled = Boolean(evmProvider); @@ -107,14 +104,14 @@ async function handleSolanaConnect() { throw new Error("No Solana browser wallet found"); } - solanaProvider = window.solana; - const connection = await solanaProvider.connect(); + const connection = await connectSolanaProvider(window.solana); + solanaProvider = connection.provider; solanaWalletInfo.textContent = - connection.publicKey?.toString() ?? - solanaProvider.publicKey?.toString() ?? - "Connected"; - bridgeButton.disabled = !evmProvider || !solanaProvider; + connection.address ?? "Connected"; + bridgeButton.disabled = !evmProvider; } catch (error) { + solanaProvider = null; + bridgeButton.disabled = true; render({ error: error instanceof Error ? error.message : "Unknown error" }); } finally { connectSolButton.disabled = Boolean(solanaProvider); diff --git a/app-kit-send/package.json b/app-kit-send/package.json index 41226b4..e54cc1a 100644 --- a/app-kit-send/package.json +++ b/app-kit-send/package.json @@ -5,11 +5,13 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "test": "vitest run", "preview": "vite preview" }, "devDependencies": { "typescript": "~7.0.2", - "vite": "^8.2.1" + "vite": "^8.2.1", + "vitest": "^4.1.9" }, "dependencies": { "@circle-fin/adapter-viem-v2": "^1.15.1", diff --git a/app-kit-send/src/connect.test.ts b/app-kit-send/src/connect.test.ts new file mode 100644 index 0000000..8ab7d27 --- /dev/null +++ b/app-kit-send/src/connect.test.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, vi } from "vitest"; +import { connectEvmProvider } from "./connect.ts"; +import type { BrowserWalletProvider } from "./connect.ts"; + +describe("connectEvmProvider", () => { + it("returns the provider only after permission and account lookup succeed", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(["0x1111111111111111111111111111111111111111"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x1111111111111111111111111111111111111111", + }); + expect(provider.request).toHaveBeenNthCalledWith(1, { + method: "eth_requestAccounts", + params: undefined, + }); + expect(provider.request).toHaveBeenNthCalledWith(2, { + method: "eth_accounts", + params: undefined, + }); + }); + + it("does not return a connected provider when permission is rejected", async () => { + const rejection = new Error("User rejected the request"); + const provider = { + request: vi.fn().mockRejectedValue(rejection), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toBe(rejection); + expect(provider.request).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app-kit-send/src/connect.ts b/app-kit-send/src/connect.ts new file mode 100644 index 0000000..97e653f --- /dev/null +++ b/app-kit-send/src/connect.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CreateViemAdapterFromProviderParams } from "@circle-fin/adapter-viem-v2"; + +export type BrowserWalletProvider = + CreateViemAdapterFromProviderParams["provider"]; + +export async function connectEvmProvider(provider: BrowserWalletProvider) { + await provider.request({ + method: "eth_requestAccounts", + params: undefined, + }); + const accounts = (await provider.request({ + method: "eth_accounts", + params: undefined, + })) as string[]; + + return { provider, account: accounts[0] }; +} diff --git a/app-kit-send/src/main.ts b/app-kit-send/src/main.ts index 9b71fcb..2c0f7e5 100644 --- a/app-kit-send/src/main.ts +++ b/app-kit-send/src/main.ts @@ -19,9 +19,8 @@ import { AppKit } from "@circle-fin/app-kit"; import type { SendParams } from "@circle-fin/app-kit"; import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2"; -import type { CreateViemAdapterFromProviderParams } from "@circle-fin/adapter-viem-v2"; - -type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"]; +import { connectEvmProvider } from "./connect.ts"; +import type { BrowserWalletProvider } from "./connect.ts"; type EIP6963ProviderDetail = { info: { @@ -72,19 +71,14 @@ async function handleWalletConnect() { try { connectWalletButton.disabled = true; - walletProvider = await getProvider(); - await walletProvider.request({ - method: "eth_requestAccounts", - params: undefined, - }); - const accounts = (await walletProvider.request({ - method: "eth_accounts", - params: undefined, - })) as string[]; + const connection = await connectEvmProvider(await getProvider()); + walletProvider = connection.provider; - walletInfo.textContent = accounts[0] ?? "Connected"; - sendButton.disabled = !walletProvider; + walletInfo.textContent = connection.account ?? "Connected"; + sendButton.disabled = false; } catch (error) { + walletProvider = null; + sendButton.disabled = true; render({ error: error instanceof Error ? error.message : "Unknown error" }); } finally { connectWalletButton.disabled = Boolean(walletProvider); diff --git a/app-kit-swap/package.json b/app-kit-swap/package.json index 3742342..d396af1 100644 --- a/app-kit-swap/package.json +++ b/app-kit-swap/package.json @@ -5,11 +5,13 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", + "test": "vitest run", "preview": "vite preview" }, "devDependencies": { "typescript": "~7.0.2", - "vite": "^8.2.1" + "vite": "^8.2.1", + "vitest": "^4.1.9" }, "dependencies": { "@circle-fin/adapter-viem-v2": "^1.15.1", diff --git a/app-kit-swap/src/connect.test.ts b/app-kit-swap/src/connect.test.ts new file mode 100644 index 0000000..313d1c9 --- /dev/null +++ b/app-kit-swap/src/connect.test.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, expect, it, vi } from "vitest"; +import { connectEvmProvider } from "./connect.ts"; +import type { BrowserWalletProvider } from "./connect.ts"; + +describe("connectEvmProvider", () => { + it("returns the provider only after permission and account lookup succeed", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce(undefined) + .mockResolvedValueOnce(["0x2222222222222222222222222222222222222222"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x2222222222222222222222222222222222222222", + }); + expect(provider.request).toHaveBeenNthCalledWith(1, { + method: "eth_requestAccounts", + params: undefined, + }); + expect(provider.request).toHaveBeenNthCalledWith(2, { + method: "eth_accounts", + params: undefined, + }); + }); + + it("does not return a connected provider when permission is rejected", async () => { + const rejection = new Error("User rejected the request"); + const provider = { + request: vi.fn().mockRejectedValue(rejection), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toBe(rejection); + expect(provider.request).toHaveBeenCalledTimes(1); + }); +}); diff --git a/app-kit-swap/src/connect.ts b/app-kit-swap/src/connect.ts new file mode 100644 index 0000000..97e653f --- /dev/null +++ b/app-kit-swap/src/connect.ts @@ -0,0 +1,35 @@ +/** + * Copyright 2026 Circle Internet Group, Inc. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import type { CreateViemAdapterFromProviderParams } from "@circle-fin/adapter-viem-v2"; + +export type BrowserWalletProvider = + CreateViemAdapterFromProviderParams["provider"]; + +export async function connectEvmProvider(provider: BrowserWalletProvider) { + await provider.request({ + method: "eth_requestAccounts", + params: undefined, + }); + const accounts = (await provider.request({ + method: "eth_accounts", + params: undefined, + })) as string[]; + + return { provider, account: accounts[0] }; +} diff --git a/app-kit-swap/src/main.ts b/app-kit-swap/src/main.ts index 82f12df..327df0a 100644 --- a/app-kit-swap/src/main.ts +++ b/app-kit-swap/src/main.ts @@ -19,9 +19,8 @@ import { AppKit } from "@circle-fin/app-kit"; import type { SwapParams } from "@circle-fin/app-kit"; import { createViemAdapterFromProvider } from "@circle-fin/adapter-viem-v2"; -import type { CreateViemAdapterFromProviderParams } from "@circle-fin/adapter-viem-v2"; - -type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"]; +import { connectEvmProvider } from "./connect.ts"; +import type { BrowserWalletProvider } from "./connect.ts"; type EIP6963ProviderDetail = { info: { @@ -72,19 +71,14 @@ async function handleWalletConnect() { try { connectWalletButton.disabled = true; - walletProvider = await getProvider(); - await walletProvider.request({ - method: "eth_requestAccounts", - params: undefined, - }); - const accounts = (await walletProvider.request({ - method: "eth_accounts", - params: undefined, - })) as string[]; + const connection = await connectEvmProvider(await getProvider()); + walletProvider = connection.provider; - walletInfo.textContent = accounts[0] ?? "Connected"; - swapButton.disabled = !walletProvider; + walletInfo.textContent = connection.account ?? "Connected"; + swapButton.disabled = false; } catch (error) { + walletProvider = null; + swapButton.disabled = true; render({ error: error instanceof Error ? error.message : "Unknown error" }); } finally { connectWalletButton.disabled = Boolean(walletProvider); From cc3debd69111e963aa9ddc244f091da6b361200e Mon Sep 17 00:00:00 2001 From: Omer Bektas Date: Sat, 5 Sep 2026 13:34:37 +0300 Subject: [PATCH 2/2] fix(examples): require an account after wallet permission --- app-kit-bridge-evm/src/connect.test.ts | 19 +++++++++++------ app-kit-bridge-evm/src/connect.ts | 12 +++++------ app-kit-bridge-solana/src/connect.test.ts | 25 ++++++++++++++++++++++- app-kit-bridge-solana/src/connect.ts | 15 ++++++++------ app-kit-send/src/connect.test.ts | 19 +++++++++++------ app-kit-send/src/connect.ts | 12 +++++------ app-kit-swap/src/connect.test.ts | 19 +++++++++++------ app-kit-swap/src/connect.ts | 12 +++++------ 8 files changed, 90 insertions(+), 43 deletions(-) diff --git a/app-kit-bridge-evm/src/connect.test.ts b/app-kit-bridge-evm/src/connect.test.ts index 777666e..9d41fd6 100644 --- a/app-kit-bridge-evm/src/connect.test.ts +++ b/app-kit-bridge-evm/src/connect.test.ts @@ -21,11 +21,10 @@ import { connectEvmProvider } from "./connect.ts"; import type { BrowserWalletProvider } from "./connect.ts"; describe("connectEvmProvider", () => { - it("returns the provider only after permission and account lookup succeed", async () => { + it("returns the provider only after permission returns an account", async () => { const provider = { request: vi .fn() - .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(["0x3333333333333333333333333333333333333333"]), } as unknown as BrowserWalletProvider; @@ -37,10 +36,7 @@ describe("connectEvmProvider", () => { method: "eth_requestAccounts", params: undefined, }); - expect(provider.request).toHaveBeenNthCalledWith(2, { - method: "eth_accounts", - params: undefined, - }); + expect(provider.request).toHaveBeenCalledTimes(1); }); it("does not return a connected provider when permission is rejected", async () => { @@ -52,4 +48,15 @@ describe("connectEvmProvider", () => { await expect(connectEvmProvider(provider)).rejects.toBe(rejection); expect(provider.request).toHaveBeenCalledTimes(1); }); + + it("does not return a connected provider when permission returns no account", async () => { + const provider = { + request: vi.fn().mockResolvedValue([]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toThrow( + "No account returned after wallet permission", + ); + expect(provider.request).toHaveBeenCalledTimes(1); + }); }); diff --git a/app-kit-bridge-evm/src/connect.ts b/app-kit-bridge-evm/src/connect.ts index 97e653f..1e1f7f9 100644 --- a/app-kit-bridge-evm/src/connect.ts +++ b/app-kit-bridge-evm/src/connect.ts @@ -22,14 +22,14 @@ export type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"]; export async function connectEvmProvider(provider: BrowserWalletProvider) { - await provider.request({ - method: "eth_requestAccounts", - params: undefined, - }); const accounts = (await provider.request({ - method: "eth_accounts", + method: "eth_requestAccounts", params: undefined, })) as string[]; + const account = accounts[0]; + if (!account) { + throw new Error("No account returned after wallet permission"); + } - return { provider, account: accounts[0] }; + return { provider, account }; } diff --git a/app-kit-bridge-solana/src/connect.test.ts b/app-kit-bridge-solana/src/connect.test.ts index db61cc9..8867e8f 100644 --- a/app-kit-bridge-solana/src/connect.test.ts +++ b/app-kit-bridge-solana/src/connect.test.ts @@ -32,7 +32,6 @@ describe("wallet connection", () => { const provider = { request: vi .fn() - .mockResolvedValueOnce(undefined) .mockResolvedValueOnce([account]), } as unknown as BrowserWalletProvider; @@ -40,6 +39,7 @@ describe("wallet connection", () => { provider, account, }); + expect(provider.request).toHaveBeenCalledTimes(1); }); it("does not return an EVM provider when permission is rejected", async () => { @@ -52,6 +52,17 @@ describe("wallet connection", () => { expect(provider.request).toHaveBeenCalledTimes(1); }); + it("does not return an EVM provider when permission returns no account", async () => { + const provider = { + request: vi.fn().mockResolvedValue([]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toThrow( + "No account returned after wallet permission", + ); + expect(provider.request).toHaveBeenCalledTimes(1); + }); + it("does not return a Solana provider when connection is rejected", async () => { const rejection = new Error("User rejected the request"); const provider = { @@ -74,4 +85,16 @@ describe("wallet connection", () => { address: "solana-address", }); }); + + it("does not return a Solana provider when connection returns no address", async () => { + const provider = { + connect: vi.fn().mockResolvedValue({}), + publicKey: null, + } as unknown as SolanaWalletProvider; + + await expect(connectSolanaProvider(provider)).rejects.toThrow( + "No address returned after wallet connection", + ); + expect(provider.connect).toHaveBeenCalledTimes(1); + }); }); diff --git a/app-kit-bridge-solana/src/connect.ts b/app-kit-bridge-solana/src/connect.ts index d58d7f1..ede0e4e 100644 --- a/app-kit-bridge-solana/src/connect.ts +++ b/app-kit-bridge-solana/src/connect.ts @@ -25,22 +25,25 @@ export type SolanaWalletProvider = CreateSolanaAdapterFromProviderParams["provider"]; export async function connectEvmProvider(provider: BrowserWalletProvider) { - await provider.request({ - method: "eth_requestAccounts", - params: undefined, - }); const accounts = (await provider.request({ - method: "eth_accounts", + method: "eth_requestAccounts", params: undefined, })) as string[]; + const account = accounts[0]; + if (!account) { + throw new Error("No account returned after wallet permission"); + } - return { provider, account: accounts[0] }; + return { provider, account }; } export async function connectSolanaProvider(provider: SolanaWalletProvider) { const connection = await provider.connect(); const address = connection.publicKey?.toString() ?? provider.publicKey?.toString(); + if (!address) { + throw new Error("No address returned after wallet connection"); + } return { provider, address }; } diff --git a/app-kit-send/src/connect.test.ts b/app-kit-send/src/connect.test.ts index 8ab7d27..a10692d 100644 --- a/app-kit-send/src/connect.test.ts +++ b/app-kit-send/src/connect.test.ts @@ -21,11 +21,10 @@ import { connectEvmProvider } from "./connect.ts"; import type { BrowserWalletProvider } from "./connect.ts"; describe("connectEvmProvider", () => { - it("returns the provider only after permission and account lookup succeed", async () => { + it("returns the provider only after permission returns an account", async () => { const provider = { request: vi .fn() - .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(["0x1111111111111111111111111111111111111111"]), } as unknown as BrowserWalletProvider; @@ -37,10 +36,7 @@ describe("connectEvmProvider", () => { method: "eth_requestAccounts", params: undefined, }); - expect(provider.request).toHaveBeenNthCalledWith(2, { - method: "eth_accounts", - params: undefined, - }); + expect(provider.request).toHaveBeenCalledTimes(1); }); it("does not return a connected provider when permission is rejected", async () => { @@ -52,4 +48,15 @@ describe("connectEvmProvider", () => { await expect(connectEvmProvider(provider)).rejects.toBe(rejection); expect(provider.request).toHaveBeenCalledTimes(1); }); + + it("does not return a connected provider when permission returns no account", async () => { + const provider = { + request: vi.fn().mockResolvedValue([]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toThrow( + "No account returned after wallet permission", + ); + expect(provider.request).toHaveBeenCalledTimes(1); + }); }); diff --git a/app-kit-send/src/connect.ts b/app-kit-send/src/connect.ts index 97e653f..1e1f7f9 100644 --- a/app-kit-send/src/connect.ts +++ b/app-kit-send/src/connect.ts @@ -22,14 +22,14 @@ export type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"]; export async function connectEvmProvider(provider: BrowserWalletProvider) { - await provider.request({ - method: "eth_requestAccounts", - params: undefined, - }); const accounts = (await provider.request({ - method: "eth_accounts", + method: "eth_requestAccounts", params: undefined, })) as string[]; + const account = accounts[0]; + if (!account) { + throw new Error("No account returned after wallet permission"); + } - return { provider, account: accounts[0] }; + return { provider, account }; } diff --git a/app-kit-swap/src/connect.test.ts b/app-kit-swap/src/connect.test.ts index 313d1c9..ef21597 100644 --- a/app-kit-swap/src/connect.test.ts +++ b/app-kit-swap/src/connect.test.ts @@ -21,11 +21,10 @@ import { connectEvmProvider } from "./connect.ts"; import type { BrowserWalletProvider } from "./connect.ts"; describe("connectEvmProvider", () => { - it("returns the provider only after permission and account lookup succeed", async () => { + it("returns the provider only after permission returns an account", async () => { const provider = { request: vi .fn() - .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(["0x2222222222222222222222222222222222222222"]), } as unknown as BrowserWalletProvider; @@ -37,10 +36,7 @@ describe("connectEvmProvider", () => { method: "eth_requestAccounts", params: undefined, }); - expect(provider.request).toHaveBeenNthCalledWith(2, { - method: "eth_accounts", - params: undefined, - }); + expect(provider.request).toHaveBeenCalledTimes(1); }); it("does not return a connected provider when permission is rejected", async () => { @@ -52,4 +48,15 @@ describe("connectEvmProvider", () => { await expect(connectEvmProvider(provider)).rejects.toBe(rejection); expect(provider.request).toHaveBeenCalledTimes(1); }); + + it("does not return a connected provider when permission returns no account", async () => { + const provider = { + request: vi.fn().mockResolvedValue([]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toThrow( + "No account returned after wallet permission", + ); + expect(provider.request).toHaveBeenCalledTimes(1); + }); }); diff --git a/app-kit-swap/src/connect.ts b/app-kit-swap/src/connect.ts index 97e653f..1e1f7f9 100644 --- a/app-kit-swap/src/connect.ts +++ b/app-kit-swap/src/connect.ts @@ -22,14 +22,14 @@ export type BrowserWalletProvider = CreateViemAdapterFromProviderParams["provider"]; export async function connectEvmProvider(provider: BrowserWalletProvider) { - await provider.request({ - method: "eth_requestAccounts", - params: undefined, - }); const accounts = (await provider.request({ - method: "eth_accounts", + method: "eth_requestAccounts", params: undefined, })) as string[]; + const account = accounts[0]; + if (!account) { + throw new Error("No account returned after wallet permission"); + } - return { provider, account: accounts[0] }; + return { provider, account }; }