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..caa3aed --- /dev/null +++ b/app-kit-bridge-evm/src/connect.test.ts @@ -0,0 +1,69 @@ +/** + * 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("prefers accounts returned by eth_requestAccounts", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce(["0x1111111111111111111111111111111111111111"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x1111111111111111111111111111111111111111", + }); + expect(provider.request).toHaveBeenCalledTimes(1); + expect(provider.request).toHaveBeenCalledWith({ + method: "eth_requestAccounts", + params: undefined, + }); + }); + + it("falls back to eth_accounts when requestAccounts returns empty", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce(["0x2222222222222222222222222222222222222222"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x2222222222222222222222222222222222222222", + }); + expect(provider.request).toHaveBeenNthCalledWith(2, { + method: "eth_accounts", + params: undefined, + }); + }); + + it("throws when no account is available so callers stay disconnected", async () => { + const provider = { + request: vi.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toThrow( + "No wallet account available", + ); + }); +}); diff --git a/app-kit-bridge-evm/src/connect.ts b/app-kit-bridge-evm/src/connect.ts new file mode 100644 index 0000000..e5b5ed5 --- /dev/null +++ b/app-kit-bridge-evm/src/connect.ts @@ -0,0 +1,49 @@ +/** + * 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"]; + +/** + * Request wallet permission and resolve a concrete account. + * Prefers accounts from eth_requestAccounts; falls back to eth_accounts. + * Throws when no account is available so callers stay disconnected. + */ +export async function connectEvmProvider(provider: BrowserWalletProvider) { + const requestedAccounts = (await provider.request({ + method: "eth_requestAccounts", + params: undefined, + })) as string[] | undefined; + + let account = requestedAccounts?.[0]; + if (!account) { + const accounts = (await provider.request({ + method: "eth_accounts", + params: undefined, + })) as string[]; + account = accounts[0]; + } + + if (!account) { + throw new Error("No wallet account available"); + } + + return { provider, account }; +} diff --git a/app-kit-bridge-evm/src/main.ts b/app-kit-bridge-evm/src/main.ts index d5b2fa8..39e9963 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; + 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..5f0879d --- /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("connectEvmProvider", () => { + it("prefers accounts returned by eth_requestAccounts", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce(["0x1111111111111111111111111111111111111111"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x1111111111111111111111111111111111111111", + }); + expect(provider.request).toHaveBeenCalledTimes(1); + }); + + it("throws when no account is available so callers stay disconnected", async () => { + const provider = { + request: vi.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toThrow( + "No wallet account available", + ); + }); +}); + +describe("connectSolanaProvider", () => { + it("returns the connected address when present", async () => { + const provider = { + connect: vi.fn().mockResolvedValue({ + publicKey: { toString: () => "So11111111111111111111111111111111111111112" }, + }), + publicKey: null, + } as unknown as SolanaWalletProvider; + + await expect(connectSolanaProvider(provider)).resolves.toEqual({ + provider, + address: "So11111111111111111111111111111111111111112", + }); + }); + + it("throws when address is missing so callers stay disconnected", async () => { + const provider = { + connect: vi.fn().mockResolvedValue({ publicKey: null }), + publicKey: null, + } as unknown as SolanaWalletProvider; + + await expect(connectSolanaProvider(provider)).rejects.toThrow( + "No Solana wallet address available", + ); + }); +}); diff --git a/app-kit-bridge-solana/src/connect.ts b/app-kit-bridge-solana/src/connect.ts new file mode 100644 index 0000000..993f5b6 --- /dev/null +++ b/app-kit-bridge-solana/src/connect.ts @@ -0,0 +1,68 @@ +/** + * 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"]; + +/** + * Request wallet permission and resolve a concrete account. + * Prefers accounts from eth_requestAccounts; falls back to eth_accounts. + * Throws when no account is available so callers stay disconnected. + */ +export async function connectEvmProvider(provider: BrowserWalletProvider) { + const requestedAccounts = (await provider.request({ + method: "eth_requestAccounts", + params: undefined, + })) as string[] | undefined; + + let account = requestedAccounts?.[0]; + if (!account) { + const accounts = (await provider.request({ + method: "eth_accounts", + params: undefined, + })) as string[]; + account = accounts[0]; + } + + if (!account) { + throw new Error("No wallet account available"); + } + + return { provider, account }; +} + +/** + * Connect a Solana provider and require a resolved address. + * Throws when the address is missing so callers stay disconnected. + */ +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 Solana wallet address available"); + } + + return { provider, address }; +} diff --git a/app-kit-bridge-solana/src/main.ts b/app-kit-bridge-solana/src/main.ts index 95e755b..2c1b1ea 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; + 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,13 @@ async function handleSolanaConnect() { throw new Error("No Solana browser wallet found"); } - solanaProvider = window.solana; - const connection = await solanaProvider.connect(); - solanaWalletInfo.textContent = - connection.publicKey?.toString() ?? - solanaProvider.publicKey?.toString() ?? - "Connected"; - bridgeButton.disabled = !evmProvider || !solanaProvider; + const connection = await connectSolanaProvider(window.solana); + solanaProvider = connection.provider; + solanaWalletInfo.textContent = connection.address; + 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..caa3aed --- /dev/null +++ b/app-kit-send/src/connect.test.ts @@ -0,0 +1,69 @@ +/** + * 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("prefers accounts returned by eth_requestAccounts", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce(["0x1111111111111111111111111111111111111111"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x1111111111111111111111111111111111111111", + }); + expect(provider.request).toHaveBeenCalledTimes(1); + expect(provider.request).toHaveBeenCalledWith({ + method: "eth_requestAccounts", + params: undefined, + }); + }); + + it("falls back to eth_accounts when requestAccounts returns empty", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce(["0x2222222222222222222222222222222222222222"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x2222222222222222222222222222222222222222", + }); + expect(provider.request).toHaveBeenNthCalledWith(2, { + method: "eth_accounts", + params: undefined, + }); + }); + + it("throws when no account is available so callers stay disconnected", async () => { + const provider = { + request: vi.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toThrow( + "No wallet account available", + ); + }); +}); diff --git a/app-kit-send/src/connect.ts b/app-kit-send/src/connect.ts new file mode 100644 index 0000000..e5b5ed5 --- /dev/null +++ b/app-kit-send/src/connect.ts @@ -0,0 +1,49 @@ +/** + * 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"]; + +/** + * Request wallet permission and resolve a concrete account. + * Prefers accounts from eth_requestAccounts; falls back to eth_accounts. + * Throws when no account is available so callers stay disconnected. + */ +export async function connectEvmProvider(provider: BrowserWalletProvider) { + const requestedAccounts = (await provider.request({ + method: "eth_requestAccounts", + params: undefined, + })) as string[] | undefined; + + let account = requestedAccounts?.[0]; + if (!account) { + const accounts = (await provider.request({ + method: "eth_accounts", + params: undefined, + })) as string[]; + account = accounts[0]; + } + + if (!account) { + throw new Error("No wallet account available"); + } + + return { provider, account }; +} diff --git a/app-kit-send/src/main.ts b/app-kit-send/src/main.ts index 9b71fcb..6f6059f 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; + 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..caa3aed --- /dev/null +++ b/app-kit-swap/src/connect.test.ts @@ -0,0 +1,69 @@ +/** + * 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("prefers accounts returned by eth_requestAccounts", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce(["0x1111111111111111111111111111111111111111"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x1111111111111111111111111111111111111111", + }); + expect(provider.request).toHaveBeenCalledTimes(1); + expect(provider.request).toHaveBeenCalledWith({ + method: "eth_requestAccounts", + params: undefined, + }); + }); + + it("falls back to eth_accounts when requestAccounts returns empty", async () => { + const provider = { + request: vi + .fn() + .mockResolvedValueOnce([]) + .mockResolvedValueOnce(["0x2222222222222222222222222222222222222222"]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).resolves.toEqual({ + provider, + account: "0x2222222222222222222222222222222222222222", + }); + expect(provider.request).toHaveBeenNthCalledWith(2, { + method: "eth_accounts", + params: undefined, + }); + }); + + it("throws when no account is available so callers stay disconnected", async () => { + const provider = { + request: vi.fn().mockResolvedValueOnce([]).mockResolvedValueOnce([]), + } as unknown as BrowserWalletProvider; + + await expect(connectEvmProvider(provider)).rejects.toThrow( + "No wallet account available", + ); + }); +}); diff --git a/app-kit-swap/src/connect.ts b/app-kit-swap/src/connect.ts new file mode 100644 index 0000000..e5b5ed5 --- /dev/null +++ b/app-kit-swap/src/connect.ts @@ -0,0 +1,49 @@ +/** + * 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"]; + +/** + * Request wallet permission and resolve a concrete account. + * Prefers accounts from eth_requestAccounts; falls back to eth_accounts. + * Throws when no account is available so callers stay disconnected. + */ +export async function connectEvmProvider(provider: BrowserWalletProvider) { + const requestedAccounts = (await provider.request({ + method: "eth_requestAccounts", + params: undefined, + })) as string[] | undefined; + + let account = requestedAccounts?.[0]; + if (!account) { + const accounts = (await provider.request({ + method: "eth_accounts", + params: undefined, + })) as string[]; + account = accounts[0]; + } + + if (!account) { + throw new Error("No wallet account available"); + } + + return { provider, account }; +} diff --git a/app-kit-swap/src/main.ts b/app-kit-swap/src/main.ts index 82f12df..3556f94 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; + swapButton.disabled = false; } catch (error) { + walletProvider = null; + swapButton.disabled = true; render({ error: error instanceof Error ? error.message : "Unknown error" }); } finally { connectWalletButton.disabled = Boolean(walletProvider);