diff --git a/packages/modal/package.json b/packages/modal/package.json index 7e9619e1c..bfec732ee 100644 --- a/packages/modal/package.json +++ b/packages/modal/package.json @@ -139,6 +139,11 @@ "require": "./dist/lib.cjs/packages/modal/src/vue/wagmi/index.js", "types": "./dist/lib.cjs/types/vue/wagmi/index.d.ts" }, + "./connectors/base-account-connector": { + "import": "./dist/lib.esm/packages/modal/src/connectors/base-account-connector/index.js", + "require": "./dist/lib.cjs/packages/modal/src/connectors/base-account-connector/index.js", + "types": "./dist/lib.cjs/types/connectors/base-account-connector/index.d.ts" + }, "./connectors/coinbase-connector": { "import": "./dist/lib.esm/packages/modal/src/connectors/coinbase-connector/index.js", "require": "./dist/lib.cjs/packages/modal/src/connectors/coinbase-connector/index.js", @@ -175,6 +180,9 @@ "vue/wagmi": [ "./dist/lib.cjs/types/vue/wagmi/index.d.ts" ], + "connectors/base-account-connector": [ + "./dist/lib.cjs/types/connectors/base-account-connector/index.d.ts" + ], "connectors/coinbase-connector": [ "./dist/lib.cjs/types/connectors/coinbase-connector/index.d.ts" ], diff --git a/packages/modal/rollup.config.mjs b/packages/modal/rollup.config.mjs index b997bfcf4..9ddb742ad 100644 --- a/packages/modal/rollup.config.mjs +++ b/packages/modal/rollup.config.mjs @@ -24,6 +24,7 @@ export const baseConfig = { "./src/vue/wagmi/index.ts", // re-exports from no-modal sdk. + "./src/connectors/base-account-connector/index.ts", "./src/connectors/coinbase-connector/index.ts", "./src/providers/xrpl-provider/index.ts", "./src/providers/ethereum-mpc-provider/index.ts", diff --git a/packages/modal/src/connectors/base-account-connector/index.ts b/packages/modal/src/connectors/base-account-connector/index.ts new file mode 100644 index 000000000..ce015014c --- /dev/null +++ b/packages/modal/src/connectors/base-account-connector/index.ts @@ -0,0 +1 @@ +export * from "@web3auth/no-modal/connectors/base-account-connector"; diff --git a/packages/modal/src/ui/constants.ts b/packages/modal/src/ui/constants.ts index e1afe1cd3..18ca64329 100644 --- a/packages/modal/src/ui/constants.ts +++ b/packages/modal/src/ui/constants.ts @@ -20,6 +20,8 @@ export const DEFAULT_LOGO_DARK = "https://images.web3auth.io/web3auth-logo-w-lig export const WALLET_CONNECT_LOGO = "https://images.web3auth.io/login-wallet-connect.svg"; +export const BASE_ACCOUNT_LOGO = "https://images.web3auth.io/login-base-account.svg"; + export const DEFAULT_PRIMARY_COLOR = "#0364FF"; export const DEFAULT_ON_PRIMARY_COLOR = "#FFFFFF"; diff --git a/packages/no-modal/package.json b/packages/no-modal/package.json index 76c98c902..d2a4323a0 100644 --- a/packages/no-modal/package.json +++ b/packages/no-modal/package.json @@ -33,12 +33,16 @@ ], "peerDependencies": { "@babel/runtime": "^7.x", + "@base-org/account": "^2.5.1", "@coinbase/wallet-sdk": "^4.3.x", "react": ">=18", "viem": ">=2.45", "vue": "^3.x" }, "peerDependenciesMeta": { + "@base-org/account": { + "optional": true + }, "@coinbase/wallet-sdk": { "optional": true }, @@ -92,6 +96,7 @@ "xrpl": "^2.14.0" }, "devDependencies": { + "@base-org/account": "^2.5.1", "@coinbase/wallet-sdk": "^4.3.7", "@types/elliptic": "6.4.18", "@types/json-rpc-random-id": "^1.0.3", @@ -139,6 +144,11 @@ "require": "./dist/lib.cjs/vue/wagmi/index.js", "types": "./dist/lib.cjs/types/vue/wagmi/index.d.ts" }, + "./connectors/base-account-connector": { + "import": "./dist/lib.esm/connectors/base-account-connector/index.js", + "require": "./dist/lib.cjs/connectors/base-account-connector/index.js", + "types": "./dist/lib.cjs/types/connectors/base-account-connector/index.d.ts" + }, "./connectors/coinbase-connector": { "import": "./dist/lib.esm/connectors/coinbase-connector/index.js", "require": "./dist/lib.cjs/connectors/coinbase-connector/index.js", @@ -172,6 +182,9 @@ "vue/solana": [ "./dist/lib.cjs/types/vue/solana/index.d.ts" ], + "connectors/base-account-connector": [ + "./dist/lib.cjs/types/connectors/base-account-connector/index.d.ts" + ], "connectors/coinbase-connector": [ "./dist/lib.cjs/types/connectors/coinbase-connector/index.d.ts" ], diff --git a/packages/no-modal/src/base/wallet/index.ts b/packages/no-modal/src/base/wallet/index.ts index e2c762d09..503ab9462 100644 --- a/packages/no-modal/src/base/wallet/index.ts +++ b/packages/no-modal/src/base/wallet/index.ts @@ -10,6 +10,7 @@ export const SOLANA_CONNECTORS = { export const EVM_CONNECTORS = { COINBASE: "coinbase", + BASE_ACCOUNT: "base-account", ...MULTI_CHAIN_CONNECTORS, } as const; @@ -27,5 +28,6 @@ export const CONNECTOR_NAMES = { [MULTI_CHAIN_CONNECTORS.AUTH]: "Auth", [MULTI_CHAIN_CONNECTORS.WALLET_CONNECT_V2]: "Wallet Connect v2", [EVM_CONNECTORS.COINBASE]: "Coinbase Smart Wallet", + [EVM_CONNECTORS.BASE_ACCOUNT]: "Base", [EVM_CONNECTORS.METAMASK]: "MetaMask", }; diff --git a/packages/no-modal/src/connectors/base-account-connector/baseAccountConnector.ts b/packages/no-modal/src/connectors/base-account-connector/baseAccountConnector.ts new file mode 100644 index 000000000..4428b1ca6 --- /dev/null +++ b/packages/no-modal/src/connectors/base-account-connector/baseAccountConnector.ts @@ -0,0 +1,236 @@ +import type { AppMetadata, Preference } from "@base-org/account"; + +import { + BaseConnectorLoginParams, + BaseConnectorSettings, + CHAIN_NAMESPACES, + ChainNamespaceType, + CONNECTED_EVENT_DATA, + CONNECTOR_CATEGORY, + CONNECTOR_CATEGORY_TYPE, + CONNECTOR_EVENTS, + CONNECTOR_NAMESPACES, + CONNECTOR_STATUS, + CONNECTOR_STATUS_TYPE, + ConnectorFn, + ConnectorInitOptions, + ConnectorNamespaceType, + ConnectorParams, + IdentityTokenInfo, + IProvider, + UserInfo, + WALLET_CONNECTOR_TYPE, + WALLET_CONNECTORS, + WalletLoginError, + Web3AuthError, +} from "../../base"; +import { BaseEvmConnector } from "../base-evm-connector"; +import { getSiteIcon, getSiteName } from "../utils"; + +export type BaseAccountSDKOptions = Partial }>; + +export interface BaseAccountConnectorOptions extends BaseConnectorSettings { + connectorSettings?: BaseAccountSDKOptions; +} + +interface BaseAccountProvider { + request(args: { method: string; params?: unknown[] }): Promise; + on(event: string, listener: (...args: unknown[]) => void): void; + once(event: string, listener: (...args: unknown[]) => void): void; + removeAllListeners(): void; +} + +interface ProviderRpcError extends Error { + code: number; +} + +class BaseAccountConnector extends BaseEvmConnector { + readonly connectorNamespace: ConnectorNamespaceType = CONNECTOR_NAMESPACES.EIP155; + + readonly currentChainNamespace: ChainNamespaceType = CHAIN_NAMESPACES.EIP155; + + readonly type: CONNECTOR_CATEGORY_TYPE = CONNECTOR_CATEGORY.EXTERNAL; + + readonly name: WALLET_CONNECTOR_TYPE = WALLET_CONNECTORS.BASE_ACCOUNT; + + public status: CONNECTOR_STATUS_TYPE = CONNECTOR_STATUS.NOT_READY; + + private baseAccountProvider: BaseAccountProvider | null = null; + + private baseAccountOptions: BaseAccountSDKOptions = { appName: "Web3Auth" }; + + constructor(connectorOptions: BaseAccountConnectorOptions) { + super(connectorOptions); + this.baseAccountOptions = { ...this.baseAccountOptions, ...connectorOptions.connectorSettings }; + } + + get provider(): IProvider | null { + if (this.status !== CONNECTOR_STATUS.NOT_READY && this.baseAccountProvider) { + return this.baseAccountProvider as unknown as IProvider; + } + return null; + } + + set provider(_: IProvider | null) { + throw new Error("Not implemented"); + } + + async init(options: ConnectorInitOptions): Promise { + await super.init(options); + const chainConfig = this.coreOptions.chains.find((x) => x.chainId === options.chainId); + super.checkInitializationRequirements({ chainConfig }); + + const { createBaseAccountSDK } = await import("@base-org/account"); + + // Derive defaults from site metadata if available + let appName = this.baseAccountOptions.appName || "Web3Auth"; + let appLogoUrl = this.baseAccountOptions.appLogoUrl || ""; + + if (typeof window !== "undefined") { + if (!this.baseAccountOptions.appName) { + appName = getSiteName(window) || "Web3Auth"; + } + if (!this.baseAccountOptions.appLogoUrl) { + appLogoUrl = (await getSiteIcon(window)) || ""; + } + } + + // Derive appChainIds from all EIP155 chains + const appChainIds = this.coreOptions.chains + .filter((x) => x.chainNamespace === CHAIN_NAMESPACES.EIP155) + .map((x) => Number.parseInt(x.chainId, 16)); + + const sdk = createBaseAccountSDK({ + ...this.baseAccountOptions, + appName, + appLogoUrl: appLogoUrl || null, + appChainIds: this.baseAccountOptions.appChainIds || appChainIds, + }); + + this.baseAccountProvider = sdk.getProvider() as unknown as BaseAccountProvider; + this.status = CONNECTOR_STATUS.READY; + this.emit(CONNECTOR_EVENTS.READY, WALLET_CONNECTORS.BASE_ACCOUNT); + + try { + if (options.autoConnect) { + this.rehydrated = true; + const provider = await this.connect({ chainId: options.chainId, getIdentityToken: options.getIdentityToken }); + if (!provider) { + this.rehydrated = false; + throw WalletLoginError.connectionError("Failed to rehydrate."); + } + } + } catch (error) { + this.emit(CONNECTOR_EVENTS.REHYDRATION_ERROR, error as Web3AuthError); + } + } + + async connect({ chainId, getIdentityToken }: BaseConnectorLoginParams): Promise { + super.checkConnectionRequirements(); + if (!this.baseAccountProvider) throw WalletLoginError.notConnectedError("Connector is not initialized"); + + this.status = CONNECTOR_STATUS.CONNECTING; + this.emit(CONNECTOR_EVENTS.CONNECTING, { connector: WALLET_CONNECTORS.BASE_ACCOUNT }); + + try { + const chainConfig = this.coreOptions.chains.find((x) => x.chainId === chainId); + if (!chainConfig) throw WalletLoginError.connectionError("Chain config is not available"); + + await this.baseAccountProvider.request({ method: "eth_requestAccounts" }); + const currentChainId = await this.baseAccountProvider.request({ method: "eth_chainId" }); + + if (currentChainId !== chainConfig.chainId) { + await this.switchChain(chainConfig, true); + } + + this.status = CONNECTOR_STATUS.CONNECTED; + if (!this.provider) throw WalletLoginError.notConnectedError("Failed to connect with provider"); + + this.provider.once("disconnect", () => { + this.disconnect(); + }); + + let identityTokenInfo: IdentityTokenInfo | undefined; + + this.emit(CONNECTOR_EVENTS.CONNECTED, { + connector: WALLET_CONNECTORS.BASE_ACCOUNT, + reconnected: this.rehydrated, + provider: this.provider, + identityTokenInfo, + } as CONNECTED_EVENT_DATA); + + if (getIdentityToken) { + identityTokenInfo = await this.getIdentityToken(); + } + + return this.provider; + } catch (error) { + this.status = CONNECTOR_STATUS.READY; + if (!this.rehydrated) this.emit(CONNECTOR_EVENTS.ERRORED, error as Web3AuthError); + this.rehydrated = false; + if (error instanceof Web3AuthError) throw error; + throw WalletLoginError.connectionError("Failed to login with Base Account", error); + } + } + + async disconnect(options: { cleanup: boolean } = { cleanup: false }): Promise { + await super.disconnectSession(); + this.provider?.removeAllListeners(); + if (options.cleanup) { + this.status = CONNECTOR_STATUS.NOT_READY; + this.baseAccountProvider = null; + } else { + this.status = CONNECTOR_STATUS.READY; + } + await super.disconnect(); + } + + async getUserInfo(): Promise> { + if (!this.canAuthorize) throw WalletLoginError.notConnectedError("Not connected with wallet, Please login/connect first"); + return {}; + } + + public async switchChain(params: { chainId: string }, init = false): Promise { + super.checkSwitchChainRequirements(params, init); + try { + await this.baseAccountProvider?.request({ method: "wallet_switchEthereumChain", params: [{ chainId: params.chainId }] }); + } catch (switchError: unknown) { + if ((switchError as ProviderRpcError).code === 4902) { + const chainConfig = this.coreOptions.chains.find((x) => x.chainId === params.chainId); + if (!chainConfig) throw WalletLoginError.connectionError("Chain config is not available"); + await this.baseAccountProvider?.request({ + method: "wallet_addEthereumChain", + params: [ + { + chainId: chainConfig.chainId, + rpcUrls: [chainConfig.rpcTarget], + chainName: chainConfig.displayName, + nativeCurrency: { name: chainConfig.tickerName, symbol: chainConfig.ticker, decimals: chainConfig.decimals || 18 }, + blockExplorerUrls: [chainConfig.blockExplorerUrl], + iconUrls: [chainConfig.logo], + }, + ], + }); + return; + } + throw switchError; + } + } + + public async enableMFA(): Promise { + throw new Error("Method Not implemented"); + } + + public async manageMFA(): Promise { + throw new Error("Method Not implemented"); + } +} + +export const baseAccountConnector = (params?: BaseAccountSDKOptions): ConnectorFn => { + return ({ coreOptions }: ConnectorParams) => { + return new BaseAccountConnector({ + connectorSettings: params, + coreOptions, + }); + }; +}; diff --git a/packages/no-modal/src/connectors/base-account-connector/index.ts b/packages/no-modal/src/connectors/base-account-connector/index.ts new file mode 100644 index 000000000..3495a651e --- /dev/null +++ b/packages/no-modal/src/connectors/base-account-connector/index.ts @@ -0,0 +1 @@ +export * from "./baseAccountConnector"; diff --git a/packages/no-modal/src/connectors/index.ts b/packages/no-modal/src/connectors/index.ts index eb7b052dc..87b640ed8 100644 --- a/packages/no-modal/src/connectors/index.ts +++ b/packages/no-modal/src/connectors/index.ts @@ -1,4 +1,5 @@ export * from "./auth-connector"; +export * from "./base-account-connector"; export * from "./base-evm-connector"; export * from "./base-solana-connector"; export * from "./injected-evm-connector"; diff --git a/packages/no-modal/src/noModal.ts b/packages/no-modal/src/noModal.ts index 698608c00..0cae4d173 100644 --- a/packages/no-modal/src/noModal.ts +++ b/packages/no-modal/src/noModal.ts @@ -865,7 +865,11 @@ export class Web3AuthNoModal extends SafeEventEmitter imp const isAaSupportedForCurrentChain = this.currentChain?.chainNamespace === CHAIN_NAMESPACES.EIP155 && accountAbstractionConfig?.chains?.some((chain) => chain.chainId === this.currentChain?.chainId); - if (isAaSupportedForCurrentChain && (data.connector === WALLET_CONNECTORS.AUTH || this.coreOptions.useAAWithExternalWallet)) { + // Skip AA wrapping for Base Account connector as it's already a smart account provider + const shouldApplyAA = + data.connector === WALLET_CONNECTORS.AUTH || + (this.coreOptions.useAAWithExternalWallet && data.connector !== WALLET_CONNECTORS.BASE_ACCOUNT); + if (isAaSupportedForCurrentChain && shouldApplyAA) { const { accountAbstractionProvider, toEoaProvider } = await import("./providers/account-abstraction-provider"); // for embedded wallets, we use ws-embed provider which is AA provider, need to derive EOA provider const eoaProvider: IProvider = data.connector === WALLET_CONNECTORS.AUTH ? await toEoaProvider(provider) : provider;