diff --git a/.changeset/many-camels-punch.md b/.changeset/many-camels-punch.md new file mode 100644 index 00000000000..859a3728494 --- /dev/null +++ b/.changeset/many-camels-punch.md @@ -0,0 +1,5 @@ +--- +"@thirdweb-dev/wallets": patch +--- + +Introduce TokenBoundSmartWallet class for ERC-6551 support diff --git a/packages/react-core/src/core/hooks/wallet-hooks.ts b/packages/react-core/src/core/hooks/wallet-hooks.ts index c8c95ee2ac8..e20f5a424d0 100644 --- a/packages/react-core/src/core/hooks/wallet-hooks.ts +++ b/packages/react-core/src/core/hooks/wallet-hooks.ts @@ -14,6 +14,7 @@ import type { RainbowWallet, SafeWallet, SmartWallet, + TokenBoundSmartWallet, TrustWallet, WalletConnect, walletIds, @@ -32,6 +33,7 @@ type WalletIdToWalletTypeMap = { magicLink: MagicLink; paper: PaperWallet; smartWallet: SmartWallet; + tokenBoundSmartWallet: TokenBoundSmartWallet; safe: SafeWallet; trust: TrustWallet; embeddedWallet: EmbeddedWallet; diff --git a/packages/wallets/evm/connectors/token-bound-smart-wallet/package.json b/packages/wallets/evm/connectors/token-bound-smart-wallet/package.json new file mode 100644 index 00000000000..dcd99dca970 --- /dev/null +++ b/packages/wallets/evm/connectors/token-bound-smart-wallet/package.json @@ -0,0 +1,7 @@ +{ + "main": "dist/thirdweb-dev-wallets-evm-connectors-token-bound-smart-wallet.cjs.js", + "module": "dist/thirdweb-dev-wallets-evm-connectors-token-bound-smart-wallet.esm.js", + "browser": { + "./dist/thirdweb-dev-wallets-evm-connectors-token-bound-smart-wallet.esm.js": "./dist/thirdweb-dev-wallets-evm-connectors-token-bound-smart-wallet.browser.esm.js" + } +} diff --git a/packages/wallets/evm/wallets/token-bound-smart-wallet/package.json b/packages/wallets/evm/wallets/token-bound-smart-wallet/package.json new file mode 100644 index 00000000000..c050e38ecb7 --- /dev/null +++ b/packages/wallets/evm/wallets/token-bound-smart-wallet/package.json @@ -0,0 +1,7 @@ +{ + "main": "dist/thirdweb-dev-wallets-evm-wallets-token-bound-smart-wallet.cjs.js", + "module": "dist/thirdweb-dev-wallets-evm-wallets-token-bound-smart-wallet.esm.js", + "browser": { + "./dist/thirdweb-dev-wallets-evm-wallets-token-bound-smart-wallet.esm.js": "./dist/thirdweb-dev-wallets-evm-wallets-token-bound-smart-wallet.browser.esm.js" + } +} diff --git a/packages/wallets/package.json b/packages/wallets/package.json index 54ffd061faa..a918a4a83c7 100644 --- a/packages/wallets/package.json +++ b/packages/wallets/package.json @@ -344,6 +344,13 @@ }, "default": "./evm/connectors/embedded-wallet/dist/thirdweb-dev-wallets-evm-connectors-embedded-wallet.cjs.js" }, + "./evm/wallets/token-bound-smart-wallet": { + "module": { + "browser": "./evm/wallets/token-bound-smart-wallet/dist/thirdweb-dev-wallets-evm-wallets-token-bound-smart-wallet.browser.esm.js", + "default": "./evm/wallets/token-bound-smart-wallet/dist/thirdweb-dev-wallets-evm-wallets-token-bound-smart-wallet.esm.js" + }, + "default": "./evm/wallets/token-bound-smart-wallet/dist/thirdweb-dev-wallets-evm-wallets-token-bound-smart-wallet.cjs.js" + }, "./evm/connectors/wallet-connect-v1": { "module": { "browser": "./evm/connectors/wallet-connect-v1/dist/thirdweb-dev-wallets-evm-connectors-wallet-connect-v1.browser.esm.js", @@ -351,6 +358,13 @@ }, "default": "./evm/connectors/wallet-connect-v1/dist/thirdweb-dev-wallets-evm-connectors-wallet-connect-v1.cjs.js" }, + "./evm/connectors/token-bound-smart-wallet": { + "module": { + "browser": "./evm/connectors/token-bound-smart-wallet/dist/thirdweb-dev-wallets-evm-connectors-token-bound-smart-wallet.browser.esm.js", + "default": "./evm/connectors/token-bound-smart-wallet/dist/thirdweb-dev-wallets-evm-connectors-token-bound-smart-wallet.esm.js" + }, + "default": "./evm/connectors/token-bound-smart-wallet/dist/thirdweb-dev-wallets-evm-connectors-token-bound-smart-wallet.cjs.js" + }, "./evm/connectors/embedded-wallet/implementations": { "module": { "browser": "./evm/connectors/embedded-wallet/implementations/dist/thirdweb-dev-wallets-evm-connectors-embedded-wallet-implementations.browser.esm.js", diff --git a/packages/wallets/src/evm/connectors/smart-wallet/index.ts b/packages/wallets/src/evm/connectors/smart-wallet/index.ts index 4180ad36812..db4141a70e3 100644 --- a/packages/wallets/src/evm/connectors/smart-wallet/index.ts +++ b/packages/wallets/src/evm/connectors/smart-wallet/index.ts @@ -1,4 +1,4 @@ -import { Chain, getChainByChainId } from "@thirdweb-dev/chains"; +import { Chain } from "@thirdweb-dev/chains"; import { ConnectParams, Connector } from "../../interfaces/connector"; import { ERC4337EthersProvider } from "./lib/erc4337-provider"; import { getVerifyingPaymaster } from "./lib/paymaster"; @@ -15,7 +15,6 @@ import { EVMWallet } from "../../interfaces"; import { ERC4337EthersSigner } from "./lib/erc4337-signer"; import { BigNumber, ethers, providers, utils } from "ethers"; import { - ChainOrRpcUrl, getChainProvider, SignerPermissionsInput, SignerWithPermissions, @@ -28,10 +27,11 @@ import { AccountAPI } from "./lib/account"; import { AddressZero } from "@account-abstraction/utils"; export class SmartWalletConnector extends Connector { - private config: SmartWalletConfig; + protected config: SmartWalletConfig; private aaProvider: ERC4337EthersProvider | undefined; private accountApi: AccountAPI | undefined; personalWallet?: EVMWallet; + chainId?: number; constructor(config: SmartWalletConfig) { super(); @@ -44,11 +44,12 @@ export class SmartWalletConnector extends Connector { clientId: config.clientId, secretKey: config.secretKey, }) as providers.BaseProvider; - const chainSlug = await this.getChainSlug(config.chain, originalProvider); + this.chainId = (await originalProvider.getNetwork()).chainId; const bundlerUrl = - this.config.bundlerUrl || `https://${chainSlug}.bundler.thirdweb.com`; + this.config.bundlerUrl || `https://${this.chainId}.bundler.thirdweb.com`; const paymasterUrl = - this.config.paymasterUrl || `https://${chainSlug}.bundler.thirdweb.com`; + this.config.paymasterUrl || + `https://${this.chainId}.bundler.thirdweb.com`; const entryPointAddress = config.entryPointAddress || ENTRYPOINT_ADDRESS; const localSigner = await params.personalWallet.getSigner(); const providerConfig: ProviderConfig = { @@ -79,6 +80,7 @@ export class SmartWalletConnector extends Connector { providerConfig, accountApi, originalProvider, + this.chainId, ); this.accountApi = accountApi; } @@ -125,7 +127,6 @@ export class SmartWalletConnector extends Connector { // eslint-disable-next-line @typescript-eslint/no-unused-vars async switchChain(chainId: number): Promise { - // TODO implement chain switching const provider = await this.getProvider(); const currentChainId = (await provider.getNetwork()).chainId; if (currentChainId !== chainId) { @@ -438,9 +439,9 @@ export class SmartWalletConnector extends Connector { return sdk.getContract(this.config.factoryAddress); } - private defaultFactoryInfo(): FactoryContractInfo { + protected defaultFactoryInfo(): FactoryContractInfo { return { - createAccount: async (factory: SmartContract, owner: string) => { + createAccount: async (factory, owner) => { return factory.prepare("createAccount", [ owner, ethers.utils.toUtf8Bytes(""), @@ -461,7 +462,7 @@ export class SmartWalletConnector extends Connector { }; } - private defaultAccountInfo(): AccountContractInfo { + protected defaultAccountInfo(): AccountContractInfo { return { execute: async (account, target, value, data) => { return account.prepare("execute", [target, value, data]); @@ -471,29 +472,4 @@ export class SmartWalletConnector extends Connector { }, }; } - - private async getChainSlug( - chainOrRpc: ChainOrRpcUrl, - provider: ethers.providers.Provider, - ): Promise { - if (typeof chainOrRpc === "object") { - return chainOrRpc.slug; - } - if (typeof chainOrRpc === "number") { - const chain = getChainByChainId(chainOrRpc); - return chain.slug; - } - if (typeof chainOrRpc === "string") { - if (chainOrRpc.startsWith("http") || chainOrRpc.startsWith("ws")) { - // if it's a url, try to get the chain id from the provider - const chainId = (await provider.getNetwork()).chainId; - const chain = getChainByChainId(chainId); - return chain.slug; - } - // otherwise its the network name - return chainOrRpc; - } else { - throw new Error(`Invalid network: ${chainOrRpc}`); - } - } } diff --git a/packages/wallets/src/evm/connectors/smart-wallet/lib/constants.ts b/packages/wallets/src/evm/connectors/smart-wallet/lib/constants.ts index 45363e4d016..bd790a5917f 100644 --- a/packages/wallets/src/evm/connectors/smart-wallet/lib/constants.ts +++ b/packages/wallets/src/evm/connectors/smart-wallet/lib/constants.ts @@ -1,4 +1,5 @@ export const ENTRYPOINT_ADDRESS = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; // v0.6 +export const ERC6551_REGISTRY = "0x02101dfB77FDE026414827Fdc604ddAF224F0921"; export const ACCOUNT_CORE_ABI = [ { diff --git a/packages/wallets/src/evm/connectors/smart-wallet/lib/provider-utils.ts b/packages/wallets/src/evm/connectors/smart-wallet/lib/provider-utils.ts index bc98e539162..c1aaf9d9631 100644 --- a/packages/wallets/src/evm/connectors/smart-wallet/lib/provider-utils.ts +++ b/packages/wallets/src/evm/connectors/smart-wallet/lib/provider-utils.ts @@ -15,13 +15,13 @@ export async function create4337Provider( config: ProviderConfig, accountApi: AccountAPI, originalProvider: providers.BaseProvider, + chainId: number, ): Promise { const entryPoint = EntryPoint__factory.connect( config.entryPointAddress, originalProvider, ); - const chainId = (await originalProvider.getNetwork()).chainId; const httpRpcClient = new HttpRpcClient( config.bundlerUrl, config.entryPointAddress, diff --git a/packages/wallets/src/evm/connectors/token-bound-smart-wallet/index.ts b/packages/wallets/src/evm/connectors/token-bound-smart-wallet/index.ts new file mode 100644 index 00000000000..cf91a139e15 --- /dev/null +++ b/packages/wallets/src/evm/connectors/token-bound-smart-wallet/index.ts @@ -0,0 +1,42 @@ +import { TokenBoundSmartWalletConfig } from "./types"; +import { ethers } from "ethers"; +import { SmartWalletConnector } from "../smart-wallet"; +import { FactoryContractInfo } from "../smart-wallet/types"; +import { ERC6551_REGISTRY } from "../smart-wallet/lib/constants"; + +export class TokenBoundSmartWalletConnector extends SmartWalletConnector { + protected tbaConfig: TokenBoundSmartWalletConfig; + + constructor(input: TokenBoundSmartWalletConfig) { + super({ + ...input, + factoryAddress: input.registryAddress || ERC6551_REGISTRY, + }); + this.tbaConfig = input; + // TODO default account implementation address + } + + protected defaultFactoryInfo(): FactoryContractInfo { + return { + createAccount: async (factory) => { + return factory.prepare("createAccount", [ + this.tbaConfig.accountImplementation, + this.chainId, + this.tbaConfig.tokenContract, + this.tbaConfig.tokenId, + this.tbaConfig.salt, + ethers.utils.toUtf8Bytes(""), + ]); + }, + getAccountAddress: async (factory) => { + return await factory.call("account", [ + this.tbaConfig.accountImplementation, + this.chainId, + this.tbaConfig.tokenContract, + this.tbaConfig.tokenId, + this.tbaConfig.salt, + ]); + }, + }; + } +} diff --git a/packages/wallets/src/evm/connectors/token-bound-smart-wallet/types.ts b/packages/wallets/src/evm/connectors/token-bound-smart-wallet/types.ts new file mode 100644 index 00000000000..aeab32443b7 --- /dev/null +++ b/packages/wallets/src/evm/connectors/token-bound-smart-wallet/types.ts @@ -0,0 +1,10 @@ +import type { BigNumberish } from "ethers"; +import { SmartWalletConfig } from "../smart-wallet/types"; + +export type TokenBoundSmartWalletConfig = { + tokenContract: string; + tokenId: BigNumberish; + accountImplementation: string; + registryAddress?: string; + salt?: BigNumberish; +} & Omit; diff --git a/packages/wallets/src/evm/constants/walletIds.ts b/packages/wallets/src/evm/constants/walletIds.ts index f03dce239db..97d17828ccf 100644 --- a/packages/wallets/src/evm/constants/walletIds.ts +++ b/packages/wallets/src/evm/constants/walletIds.ts @@ -8,6 +8,7 @@ export const walletIds = { paper: "paper", rainbow: "rainbowWallet", smartWallet: "smartWallet", + tokenBoundSmartWallet: "tokenBoundSmartWallet", safe: "safe", trust: "trust", embeddedWallet: "embeddedWallet", diff --git a/packages/wallets/src/evm/index.ts b/packages/wallets/src/evm/index.ts index a844fefe661..beddf47653b 100644 --- a/packages/wallets/src/evm/index.ts +++ b/packages/wallets/src/evm/index.ts @@ -44,6 +44,7 @@ export * from "./wallets/trust"; export * from "./wallets/wallet-connect"; export * from "./wallets/wallet-connect-v1"; export * from "./wallets/zerion"; +export * from "./wallets/token-bound-smart-wallet"; export { OKXWallet, type OKXWalletOptions } from "./wallets/okx"; export { getInjectedOKXProvider } from "./connectors/okx/getInjectedOKXProvider"; diff --git a/packages/wallets/src/evm/wallets/smart-wallet.ts b/packages/wallets/src/evm/wallets/smart-wallet.ts index 97b09a0cd76..ff887a16384 100644 --- a/packages/wallets/src/evm/wallets/smart-wallet.ts +++ b/packages/wallets/src/evm/wallets/smart-wallet.ts @@ -37,7 +37,7 @@ export class SmartWallet connector?: SmartWalletConnectorType; public enableConnectApp: boolean = false; - #wcWallet: WalletConnectHandler; + protected wcWallet: WalletConnectHandler; static meta = { name: "Smart Wallet", @@ -47,7 +47,7 @@ export class SmartWallet static id = walletIds.smartWallet as string; public get walletName() { - return "Smart Wallet" as const; + return "Smart Wallet"; } constructor(options: WalletOptions) { @@ -65,7 +65,7 @@ export class SmartWallet }); this.enableConnectApp = options?.enableConnectApp || false; - this.#wcWallet = this.enableConnectApp + this.wcWallet = this.enableConnectApp ? new WalletConnectV2Handler({ walletConnectWalletMetadata: options?.walletConnectWalletMetadata, walletConnectV2ProjectId: options?.walletConnectV2ProjectId, @@ -77,9 +77,9 @@ export class SmartWallet async getConnector(): Promise { if (!this.connector) { if (this.enableConnectApp) { - await this.#wcWallet.init(); + await this.wcWallet.init(); - this.#setupWalletConnectEventsListeners(); + this.setupWalletConnectEventsListeners(); } const { SmartWalletConnector } = await import( @@ -303,64 +303,64 @@ export class SmartWallet throw new Error("enableConnectApp is set to false in this wallet config"); } - this.#wcWallet?.connectApp(uri); + this.wcWallet?.connectApp(uri); } async approveSession(): Promise { - await this.#wcWallet.approveSession(this); + await this.wcWallet.approveSession(this); this.emit("message", { type: "session_approved" }); } rejectSession() { - return this.#wcWallet.rejectSession(); + return this.wcWallet.rejectSession(); } approveRequest() { - return this.#wcWallet.approveEIP155Request(this); + return this.wcWallet.approveEIP155Request(this); } rejectRequest() { - return this.#wcWallet.rejectEIP155Request(); + return this.wcWallet.rejectEIP155Request(); } getActiveSessions(): WCSession[] { - if (!this.#wcWallet) { + if (!this.wcWallet) { throw new Error( "Please, init the wallet before making session requests.", ); } - return this.#wcWallet.getActiveSessions(); + return this.wcWallet.getActiveSessions(); } disconnectSession(): Promise { - return this.#wcWallet?.disconnectSession(); + return this.wcWallet?.disconnectSession(); } isWCReceiverEnabled() { return this.enableConnectApp; } - #setupWalletConnectEventsListeners() { - if (!this.#wcWallet) { + setupWalletConnectEventsListeners() { + if (!this.wcWallet) { throw new Error( "Please, init the wallet before making session requests.", ); } - this.#wcWallet.on("session_proposal", (proposal: WCProposal) => { + this.wcWallet.on("session_proposal", (proposal: WCProposal) => { this.emit("message", { type: "session_proposal", data: proposal, }); }); - this.#wcWallet.on("session_delete", () => { + this.wcWallet.on("session_delete", () => { this.emit("message", { type: "session_delete" }); }); - this.#wcWallet.on("switch_chain", (request: WCRequest) => { + this.wcWallet.on("switch_chain", (request: WCRequest) => { const chainId = request.params[0].chainId; this.emit("message", { @@ -368,10 +368,10 @@ export class SmartWallet data: { chainId }, }); - this.#wcWallet.disconnectSession(); + this.wcWallet.disconnectSession(); }); - this.#wcWallet.on("session_request", (request: WCRequest) => { + this.wcWallet.on("session_request", (request: WCRequest) => { this.emit("message", { type: "session_request", data: request, diff --git a/packages/wallets/src/evm/wallets/token-bound-smart-wallet.ts b/packages/wallets/src/evm/wallets/token-bound-smart-wallet.ts new file mode 100644 index 00000000000..02a3f97f94b --- /dev/null +++ b/packages/wallets/src/evm/wallets/token-bound-smart-wallet.ts @@ -0,0 +1,47 @@ +import { SmartWallet } from "./smart-wallet"; +import type { TokenBoundSmartWalletConnector as TokenBoundSmartWalletConnectorType } from "../connectors/token-bound-smart-wallet"; +import { walletIds } from "../constants/walletIds"; +import type { TokenBoundSmartWalletConfig } from "../connectors/token-bound-smart-wallet/types"; +import { WalletOptions } from "./base"; +import { ERC6551_REGISTRY } from "../connectors/smart-wallet/lib/constants"; + +/** + * A smart wallet controlled by the holder of a particular NFT. + */ +export class TokenBoundSmartWallet extends SmartWallet { + tbaConnector?: TokenBoundSmartWalletConnectorType; + tbaOptions: TokenBoundSmartWalletConfig; + + static meta = { + name: "Token Bound Smart Wallet", + iconURL: + "ipfs://QmeAJVqn17aDNQhjEU3kcWVZCFBrfta8LzaDGkS8Egdiyk/smart-wallet.svg", + }; + + static id = walletIds.tokenBoundSmartWallet; + public get walletName() { + return "Token Bound Smart Wallet"; + } + + constructor(options: WalletOptions) { + super({ + ...options, + factoryAddress: options.registryAddress || ERC6551_REGISTRY, + }); + this.tbaOptions = options; + } + + async getConnector(): Promise { + if (!this.tbaConnector) { + if (this.enableConnectApp) { + await this.wcWallet.init(); + this.setupWalletConnectEventsListeners(); + } + const { TokenBoundSmartWalletConnector } = await import( + "../connectors/token-bound-smart-wallet" + ); + this.tbaConnector = new TokenBoundSmartWalletConnector(this.tbaOptions); + } + return this.tbaConnector; + } +}