From 72bed3c37bc36eed49bcd7d04773891b8bd9470e Mon Sep 17 00:00:00 2001 From: ljunb Date: Sun, 17 Nov 2024 22:23:29 +0800 Subject: [PATCH 1/3] feat: integrate FoxWallet --- .../wallet-strategy/WalletStrategy.ts | 6 + .../strategies/FoxWallet/index.ts | 280 ++++++++++++++++++ .../strategies/FoxWallet/utils.ts | 66 +++++ .../FoxWalletCosmos/foxwallet/index.ts | 210 +++++++++++++ .../strategies/FoxWalletCosmos/index.ts | 279 +++++++++++++++++ .../src/strategies/wallet-strategy/types.ts | 2 + .../src/strategies/wallet-strategy/utils.ts | 1 + packages/wallet-ts/src/types/enums.ts | 2 + .../wallets/wallet-base/src/types/enums.ts | 2 + .../wallets/wallet-base/src/types/provider.ts | 2 + .../wallets/wallet-base/src/utils/wallet.ts | 1 + .../wallet-evm/src/strategy/strategy.ts | 3 + .../src/strategy/utils/foxwallet.ts | 64 ++++ .../wallet-evm/src/strategy/utils/index.ts | 1 + .../src/strategy/WalletStrategy.ts | 5 + 15 files changed, 924 insertions(+) create mode 100644 packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts create mode 100644 packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/utils.ts create mode 100644 packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/foxwallet/index.ts create mode 100644 packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts create mode 100644 packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts b/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts index 8446fae4c..2affdd29b 100644 --- a/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts +++ b/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts @@ -19,6 +19,8 @@ import WalletConnect from './strategies/WalletConnect' import LedgerLive from './strategies/Ledger/LedgerLive' import LedgerLegacy from './strategies/Ledger/LedgerLegacy' import Magic from './strategies/Magic' +import FoxWallet from './strategies/FoxWallet' +import FoxWalletCosmos from './strategies/FoxWalletCosmos' import { isEthWallet, isCosmosWallet } from './utils' import { Wallet, WalletDeviceType } from '../../types/enums' import { MagicMetadata, SendTransactionOptions } from './types' @@ -101,6 +103,10 @@ const createStrategy = ({ return new Okx(ethWalletArgs) case Wallet.BitGet: return new BitGet(ethWalletArgs) + case Wallet.FoxWallet: + return new FoxWallet(ethWalletArgs) + case Wallet.FoxWalletCosmos: + return new FoxWalletCosmos({ ...args }) case Wallet.WalletConnect: return new WalletConnect({ ...ethWalletArgs, diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts new file mode 100644 index 000000000..9c95fccb0 --- /dev/null +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts @@ -0,0 +1,280 @@ +/* eslint-disable class-methods-use-this */ +import { sleep } from '@injectivelabs/utils' +import { AccountAddress, EthereumChainId } from '@injectivelabs/ts-types' +import { + ErrorType, + CosmosWalletException, + WalletException, + UnspecifiedErrorCode, + TransactionException, +} from '@injectivelabs/exceptions' +import { DirectSignResponse } from '@cosmjs/proto-signing' +import { TxRaw, toUtf8, TxGrpcApi, TxResponse } from '@injectivelabs/sdk-ts' +import { + ConcreteWalletStrategy, + EthereumWalletStrategyArgs, +} from '../../../types' +import { BrowserEip1993Provider, SendTransactionOptions } from '../../types' +import BaseConcreteStrategy from './../Base' +import { WalletAction, WalletDeviceType } from '../../../../types/enums' +import { getFoxWalletProvider } from './utils' + +export default class FoxWallet + extends BaseConcreteStrategy + implements ConcreteWalletStrategy +{ + constructor(args: EthereumWalletStrategyArgs) { + super(args) + } + + async getWalletDeviceType(): Promise { + return Promise.resolve(WalletDeviceType.Browser) + } + + async enable(): Promise { + return Promise.resolve(true) + } + + async getAddresses(): Promise { + const ethereum = await this.getEthereum() + + try { + return await ethereum.request({ + method: 'eth_requestAccounts', + }) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.GetAccounts, + }) + } + } + + // eslint-disable-next-line class-methods-use-this + async getSessionOrConfirm(address: AccountAddress): Promise { + return Promise.resolve( + `0x${Buffer.from( + `Confirmation for ${address} at time: ${Date.now()}`, + ).toString('hex')}`, + ) + } + + async sendEthereumTransaction( + transaction: unknown, + _options: { address: AccountAddress; ethereumChainId: EthereumChainId }, + ): Promise { + const ethereum = await this.getEthereum() + + try { + return await ethereum.request({ + method: 'eth_sendTransaction', + params: [transaction], + }) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.SendEthereumTransaction, + }) + } + } + + async sendTransaction( + transaction: TxRaw, + options: SendTransactionOptions, + ): Promise { + const { endpoints, txTimeout } = options + + if (!endpoints) { + throw new WalletException( + new Error( + 'You have to pass endpoints within the options for using Ethereum native wallets', + ), + ) + } + + const txApi = new TxGrpcApi(endpoints.grpc) + const response = await txApi.broadcast(transaction, { txTimeout }) + + if (response.code !== 0) { + throw new TransactionException(new Error(response.rawLog), { + code: UnspecifiedErrorCode, + contextCode: response.code, + contextModule: response.codespace, + }) + } + + return response + } + + /** @deprecated */ + async signTransaction( + eip712json: string, + address: AccountAddress, + ): Promise { + return this.signEip712TypedData(eip712json, address) + } + + async signEip712TypedData( + eip712json: string, + address: AccountAddress, + ): Promise { + const ethereum = await this.getEthereum() + + try { + return await ethereum.request({ + method: 'eth_signTypedData_v4', + params: [eip712json, address], + }) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.SignTransaction, + }) + } + } + + async signAminoCosmosTransaction(_transaction: { + signDoc: any + accountNumber: number + chainId: string + address: string + }): Promise { + throw new WalletException( + new Error('This wallet does not support signing Cosmos transactions'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.SendTransaction, + }, + ) + } + + // eslint-disable-next-line class-methods-use-this + async signCosmosTransaction(_transaction: { + txRaw: TxRaw + accountNumber: number + chainId: string + address: string + }): Promise { + throw new WalletException( + new Error('This wallet does not support signing Cosmos transactions'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.SendTransaction, + }, + ) + } + + async signArbitrary( + signer: AccountAddress, + data: string | Uint8Array, + ): Promise { + const ethereum = await this.getEthereum() + + try { + const signature = await ethereum.request({ + method: 'personal_sign', + params: [toUtf8(data), signer], + }) + + return signature + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.SignArbitrary, + }) + } + } + + async getEthereumChainId(): Promise { + const ethereum = await this.getEthereum() + + try { + return ethereum.request({ method: 'eth_chainId' }) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.GetChainId, + }) + } + } + + async getEthereumTransactionReceipt(txHash: string): Promise { + const ethereum = await this.getEthereum() + + const interval = 1000 + const transactionReceiptRetry = async () => { + const receipt = await ethereum.request({ + method: 'eth_getTransactionReceipt', + params: [txHash], + }) + + if (!receipt) { + await sleep(interval) + await transactionReceiptRetry() + } + + return receipt + } + + try { + return await transactionReceiptRetry() + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + type: ErrorType.WalletError, + contextModule: WalletAction.GetEthereumTransactionReceipt, + }) + } + } + + // eslint-disable-next-line class-methods-use-this + async getPubKey(): Promise { + throw new WalletException( + new Error('You can only fetch PubKey from Cosmos native wallets'), + ) + } + + onChainIdChanged(_callback: () => void): void { + // + } + + onAccountChange(_callback: (account: AccountAddress) => void): void { + // + } + + cancelOnChainIdChange(): void { + // + } + + cancelOnAccountChange(): void { + // + } + + cancelAllEvents(): void { + // + } + + private async getEthereum(): Promise { + const provider = await getFoxWalletProvider() + + if (!provider) { + throw new CosmosWalletException( + new Error('Please install the FoxWallet wallet extension.'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + contextModule: WalletAction.GetAccounts, + }, + ) + } + + return provider + } +} diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/utils.ts b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/utils.ts new file mode 100644 index 000000000..c1a2fed44 --- /dev/null +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/utils.ts @@ -0,0 +1,66 @@ +import { isServerSide } from '@injectivelabs/sdk-ts' +import { BrowserEip1993Provider, WindowWithEip1193Provider } from '../../types' + +const $window = (isServerSide() + ? {} + : window) as unknown as WindowWithEip1193Provider + +export async function getFoxWalletProvider({ timeout } = { timeout: 3000 }) { + const provider = getFoxWalletFromWindow() + + if (provider) { + return provider + } + + return listenForFoxWalletInitialized({ + timeout, + }) as Promise +} + +async function listenForFoxWalletInitialized( + { timeout } = { timeout: 3000 }, +) { + return new Promise((resolve) => { + const handleInitialization = () => { + resolve(getFoxWalletFromWindow()) + } + + $window.addEventListener('foxwallet#initialized', handleInitialization, { + once: true, + }) + + setTimeout(() => { + $window.removeEventListener( + 'foxwallet#initialized', + handleInitialization, + ) + resolve(null) + }, timeout) + }) +} + +function getFoxWalletFromWindow() { + const injectedProviderExist = + typeof window !== 'undefined' && + (typeof $window.ethereum !== 'undefined' || + typeof $window.foxwallet !== 'undefined') + + // No injected providers exist. + if (!injectedProviderExist) { + return + } + + if ($window.foxwallet) { + return $window.foxwallet + } + + if ($window.ethereum.isFoxWallet) { + return $window.ethereum + } + + if ($window.providers) { + return $window.providers.find((p) => p.isFoxWallet) + } + + return +} diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/foxwallet/index.ts b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/foxwallet/index.ts new file mode 100644 index 000000000..21c564f59 --- /dev/null +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/foxwallet/index.ts @@ -0,0 +1,210 @@ +/* eslint-disable class-methods-use-this */ +import type { Keplr as Fox } from '@keplr-wallet/types' +import type { OfflineDirectSigner } from '@cosmjs/proto-signing' +import { BroadcastMode } from '@cosmjs/launchpad' +import { + ChainId, + CosmosChainId, + TestnetCosmosChainId, +} from '@injectivelabs/ts-types' +import { + ErrorType, + CosmosWalletException, + TransactionException, + UnspecifiedErrorCode, + WalletErrorActionModule, +} from '@injectivelabs/exceptions' +import { CosmosTxV1Beta1Tx } from '@injectivelabs/sdk-ts' + +const $window = (typeof window !== 'undefined' ? window : {}) as Window & { + foxwallet?: { cosmos?: Fox } +} + +export class FoxWallet { + private chainId: CosmosChainId | TestnetCosmosChainId | ChainId + + constructor(chainId: CosmosChainId | TestnetCosmosChainId | ChainId) { + this.chainId = chainId + } + + static async isChainIdSupported(chainId: CosmosChainId): Promise { + return new FoxWallet(chainId).checkChainIdSupport() + } + + async getFoxWallet() { + const { chainId } = this + const foxwallet = this.getFox() + + try { + await foxwallet.enable(chainId) + + return foxwallet as Fox + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message)) + } + } + + async getAccounts() { + const { chainId } = this + const foxwallet = this.getFox() + + try { + return foxwallet.getOfflineSigner(chainId).getAccounts() + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + contextModule: WalletErrorActionModule.GetAccounts, + }) + } + } + + async getKey(): Promise<{ + name: string + algo: string + pubKey: Uint8Array + address: Uint8Array + bech32Address: string + }> { + const foxwallet = await this.getFoxWallet() + + try { + return foxwallet.getKey(this.chainId) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + contextModule: 'FoxWallet', + }) + } + } + + async getOfflineSigner(): Promise { + const { chainId } = this + const foxwallet = await this.getFoxWallet() + + try { + return foxwallet.getOfflineSigner( + chainId, + ) as unknown as OfflineDirectSigner + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + contextModule: 'FoxWallet', + }) + } + } + + /** + * This method is used to broadcast a transaction to the network. + * Since it uses the `Sync` mode, it will not wait for the transaction to be included in a block, + * so we have to make sure the transaction is included in a block after its broadcasted + * + * @param txRaw - raw transaction to broadcast + * @returns tx hash + */ + async broadcastTx(txRaw: CosmosTxV1Beta1Tx.TxRaw): Promise { + const { chainId } = this + const foxwallet = await this.getFoxWallet() + + try { + const result = await foxwallet.sendTx( + chainId, + CosmosTxV1Beta1Tx.TxRaw.encode(txRaw).finish(), + BroadcastMode.Sync, + ) + + if (!result || result.length === 0) { + throw new TransactionException( + new Error('Transaction failed to be broadcasted'), + { contextModule: 'FoxWallet' }, + ) + } + + return Buffer.from(result).toString('hex') + } catch (e) { + if (e instanceof TransactionException) { + throw e + } + + throw new CosmosWalletException(new Error((e as any).message), { + context: 'broadcast-tx', + contextModule: 'FoxWallet', + }) + } + } + + /** + * This method is used to broadcast a transaction to the network. + * Since it uses the `Block` mode, and it will wait for the transaction to be included in a block, + * + * @param txRaw - raw transaction to broadcast + * @returns tx hash + */ + async broadcastTxBlock(txRaw: CosmosTxV1Beta1Tx.TxRaw): Promise { + const { chainId } = this + const foxwallet = await this.getFoxWallet() + + try { + const result = await foxwallet.sendTx( + chainId, + CosmosTxV1Beta1Tx.TxRaw.encode(txRaw).finish(), + BroadcastMode.Block, + ) + + if (!result || result.length === 0) { + throw new TransactionException( + new Error('Transaction failed to be broadcasted'), + { contextModule: 'FoxWallet' }, + ) + } + + return Buffer.from(result).toString('hex') + } catch (e) { + if (e instanceof TransactionException) { + throw e + } + + throw new CosmosWalletException(new Error((e as any).message), { + context: 'broadcast-tx', + contextModule: 'FoxWallet', + }) + } + } + + public async checkChainIdSupport() { + const { chainId } = this + const foxwallet = this.getFox() + + try { + return !!(await foxwallet.getKey(chainId)) + } catch (e) { + throw new CosmosWalletException( + new Error( + `FoxWallet doesn't support ${chainId} network. Please use another Cosmos wallet`, + ), + ) + } + } + + private getFox() { + if (!$window) { + throw new CosmosWalletException( + new Error('Please install FoxWallet extension'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + contextModule: 'FoxWallet', + }, + ) + } + + if (!$window.foxwallet?.cosmos) { + throw new CosmosWalletException( + new Error('Please install FoxWallet extension'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + contextModule: 'FoxWallet', + }, + ) + } + + return $window.foxwallet.cosmos! + } +} diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts new file mode 100644 index 000000000..aaea1d84a --- /dev/null +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts @@ -0,0 +1,279 @@ +/* eslint-disable class-methods-use-this */ +import { + ChainId, + CosmosChainId, + AccountAddress, + EthereumChainId, +} from '@injectivelabs/ts-types' +import { + ErrorType, + UnspecifiedErrorCode, + CosmosWalletException, + TransactionException, +} from '@injectivelabs/exceptions' +import { + TxRaw, + TxResponse, + waitTxBroadcasted, + createTxRawFromSigResponse, + createSignDocFromTransaction, +} from '@injectivelabs/sdk-ts' +import type { DirectSignResponse } from '@cosmjs/proto-signing' +import { ConcreteWalletStrategy } from '../../../types' +import BaseConcreteStrategy from '../Base' +import { + WalletAction, + WalletDeviceType, + WalletEventListener, +} from '../../../../types/enums' +import { SendTransactionOptions } from '../../types' +import { createCosmosSignDocFromSignDoc } from '../../../../utils/cosmos' +import { FoxWallet } from './foxwallet' + +export default class FoxWalletCosmos + extends BaseConcreteStrategy + implements ConcreteWalletStrategy +{ + private foxwallet: FoxWallet + + constructor(args: { + chainId: ChainId + endpoints?: { rest: string; rpc: string } + }) { + super(args) + this.chainId = args.chainId || CosmosChainId.Injective + this.foxwallet = new FoxWallet(args.chainId) + } + + async getWalletDeviceType(): Promise { + return Promise.resolve(WalletDeviceType.Browser) + } + + async enable(): Promise { + const foxwallet = this.getFoxWallet() + + return await foxwallet.checkChainIdSupport() + } + + public async disconnect() { + if (this.listeners[WalletEventListener.AccountChange]) { + window.removeEventListener( + 'fox_keystorechange', + this.listeners[WalletEventListener.AccountChange], + ) + } + + this.listeners = {} + } + + async getAddresses(): Promise { + const foxwallet = this.getFoxWallet() + + try { + const accounts = await foxwallet.getAccounts() + + return accounts.map((account) => account.address) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: WalletAction.GetAccounts, + }) + } + } + + async getSessionOrConfirm(address: AccountAddress): Promise { + return Promise.resolve( + `0x${Buffer.from( + `Confirmation for ${address} at time: ${Date.now()}`, + ).toString('hex')}`, + ) + } + + // eslint-disable-next-line class-methods-use-this + async sendEthereumTransaction( + _transaction: unknown, + _options: { address: AccountAddress; ethereumChainId: EthereumChainId }, + ): Promise { + throw new CosmosWalletException( + new Error( + 'sendEthereumTransaction is not supported. FoxWallet only supports sending cosmos transactions', + ), + { + code: UnspecifiedErrorCode, + context: WalletAction.SendEthereumTransaction, + }, + ) + } + + async sendTransaction( + transaction: DirectSignResponse | TxRaw, + options: SendTransactionOptions, + ): Promise { + const { foxwallet } = this + const txRaw = createTxRawFromSigResponse(transaction) + + if (!options.endpoints) { + throw new CosmosWalletException( + new Error( + 'You have to pass endpoints within the options to broadcast transaction', + ), + ) + } + + try { + const txHash = await foxwallet.broadcastTx(txRaw) + + return await waitTxBroadcasted(txHash, options) + } catch (e: unknown) { + if (e instanceof TransactionException) { + throw e + } + + throw new TransactionException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }) + } + } + + /** @deprecated */ + async signTransaction( + transaction: { txRaw: TxRaw; accountNumber: number; chainId: string }, + injectiveAddress: AccountAddress, + ) { + return this.signCosmosTransaction({ + ...transaction, + address: injectiveAddress, + }) + } + + async signAminoCosmosTransaction(_transaction: { + signDoc: any + accountNumber: number + chainId: string + address: string + }): Promise { + throw new CosmosWalletException( + new Error('This wallet does not support signing using amino'), + { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }, + ) + } + + async signCosmosTransaction(transaction: { + txRaw: TxRaw + accountNumber: number + chainId: string + address: AccountAddress + }) { + const foxwallet = this.getFoxWallet() + const signer = await foxwallet.getOfflineSigner() + const signDoc = createSignDocFromTransaction(transaction) + + try { + return await signer.signDirect( + transaction.address, + createCosmosSignDocFromSignDoc(signDoc), + ) + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }) + } + } + + async signArbitrary( + signer: string, + data: string | Uint8Array, + ): Promise { + const foxwallet = this.getFoxWallet() + const fox = await foxwallet.getFoxWallet() + + try { + const signature = await fox.signArbitrary(this.chainId, signer, data) + + return signature.signature + } catch (e: unknown) { + throw new CosmosWalletException(new Error((e as any).message), { + code: UnspecifiedErrorCode, + context: WalletAction.SignArbitrary, + }) + } + } + + async signEip712TypedData( + _eip712TypedData: string, + _address: AccountAddress, + ): Promise { + throw new CosmosWalletException( + new Error('This wallet does not support signing Ethereum transactions'), + { + code: UnspecifiedErrorCode, + context: WalletAction.SendTransaction, + }, + ) + } + + async getEthereumChainId(): Promise { + throw new CosmosWalletException( + new Error('getEthereumChainId is not supported on FoxWallet'), + { + code: UnspecifiedErrorCode, + context: WalletAction.GetChainId, + }, + ) + } + + async getEthereumTransactionReceipt(_txHash: string): Promise { + throw new CosmosWalletException( + new Error('getEthereumTransactionReceipt is not supported on FoxWallet'), + { + code: UnspecifiedErrorCode, + context: WalletAction.GetEthereumTransactionReceipt, + }, + ) + } + + async getPubKey(): Promise { + const foxwallet = this.getFoxWallet() + const key = await foxwallet.getKey() + + return Buffer.from(key.pubKey).toString('base64') + } + + async onAccountChange( + callback: (account: AccountAddress) => void, + ): Promise { + const listener = async () => { + const [account] = await this.getAddresses() + + callback(account) + } + + this.listeners = { + [WalletEventListener.AccountChange]: listener, + } + + window.addEventListener('fox_keystorechange', listener) + } + + private getFoxWallet(): FoxWallet { + const { foxwallet } = this + + if (!foxwallet) { + throw new CosmosWalletException( + new Error('Please install the FoxWallet extension'), + { + code: UnspecifiedErrorCode, + type: ErrorType.WalletNotInstalledError, + context: WalletAction.SignTransaction, + }, + ) + } + + return foxwallet + } +} diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/types.ts b/packages/wallet-ts/src/strategies/wallet-strategy/types.ts index 5b59d6917..4da9754d5 100644 --- a/packages/wallet-ts/src/strategies/wallet-strategy/types.ts +++ b/packages/wallet-ts/src/strategies/wallet-strategy/types.ts @@ -9,6 +9,7 @@ export interface BrowserEip1993Provider extends Eip1993Provider { isTrust: boolean isOkxWallet: boolean isPhantom: boolean + isFoxWallet: boolean } export interface WindowWithEip1193Provider extends Window { @@ -18,6 +19,7 @@ export interface WindowWithEip1193Provider extends Window { providers: BrowserEip1993Provider[] trustWallet?: BrowserEip1993Provider phantom?: { ethereum?: BrowserEip1993Provider } + foxwallet?: BrowserEip1993Provider } export interface WindowWithLedgerSupport extends Window { diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/utils.ts b/packages/wallet-ts/src/strategies/wallet-strategy/utils.ts index adadd50ca..c3c3f88ee 100644 --- a/packages/wallet-ts/src/strategies/wallet-strategy/utils.ts +++ b/packages/wallet-ts/src/strategies/wallet-strategy/utils.ts @@ -15,6 +15,7 @@ export const isEthWallet = (wallet: Wallet): boolean => Wallet.LedgerLegacy, Wallet.WalletConnect, Wallet.CosmostationEth, + Wallet.FoxWallet, ].includes(wallet) export const isCosmosWallet = (wallet: Wallet): boolean => !isEthWallet(wallet) diff --git a/packages/wallet-ts/src/types/enums.ts b/packages/wallet-ts/src/types/enums.ts index 3f0364534..5d34177d0 100644 --- a/packages/wallet-ts/src/types/enums.ts +++ b/packages/wallet-ts/src/types/enums.ts @@ -19,6 +19,8 @@ export enum Wallet { LedgerLegacy = 'ledger-legacy', WalletConnect = 'wallet-connect', CosmostationEth = 'cosmostation-eth', + FoxWallet = 'fox-wallet', + FoxWalletCosmos = 'fox-wallet-cosmos', } export enum MagicProvider { diff --git a/packages/wallets/wallet-base/src/types/enums.ts b/packages/wallets/wallet-base/src/types/enums.ts index cbfb99842..6ceb1767b 100644 --- a/packages/wallets/wallet-base/src/types/enums.ts +++ b/packages/wallets/wallet-base/src/types/enums.ts @@ -20,6 +20,8 @@ export enum Wallet { LedgerLegacy = 'ledger-legacy', WalletConnect = 'wallet-connect', CosmostationEth = 'cosmostation-eth', + FoxWallet = 'fox-wallet', + FoxWalletCosmos = 'fox-wallet-cosmos', } export enum MagicProvider { diff --git a/packages/wallets/wallet-base/src/types/provider.ts b/packages/wallets/wallet-base/src/types/provider.ts index 0cf98d34e..474181277 100644 --- a/packages/wallets/wallet-base/src/types/provider.ts +++ b/packages/wallets/wallet-base/src/types/provider.ts @@ -7,6 +7,7 @@ export interface BrowserEip1993Provider extends Eip1993Provider { isTrust: boolean isOkxWallet: boolean isPhantom: boolean + isFoxWallet: boolean } export interface WindowWithEip1193Provider extends Window { @@ -16,4 +17,5 @@ export interface WindowWithEip1193Provider extends Window { providers: BrowserEip1993Provider[] trustWallet?: BrowserEip1993Provider phantom?: { ethereum?: BrowserEip1993Provider } + foxwallet?: BrowserEip1993Provider } diff --git a/packages/wallets/wallet-base/src/utils/wallet.ts b/packages/wallets/wallet-base/src/utils/wallet.ts index 57dfa0140..199113834 100644 --- a/packages/wallets/wallet-base/src/utils/wallet.ts +++ b/packages/wallets/wallet-base/src/utils/wallet.ts @@ -15,6 +15,7 @@ export const isEthWallet = (wallet: Wallet): boolean => Wallet.LedgerLegacy, Wallet.WalletConnect, Wallet.CosmostationEth, + Wallet.FoxWallet, ].includes(wallet) export const isCosmosWallet = (wallet: Wallet): boolean => !isEthWallet(wallet) diff --git a/packages/wallets/wallet-evm/src/strategy/strategy.ts b/packages/wallets/wallet-evm/src/strategy/strategy.ts index 726fd6f4b..eaa0150df 100644 --- a/packages/wallets/wallet-evm/src/strategy/strategy.ts +++ b/packages/wallets/wallet-evm/src/strategy/strategy.ts @@ -40,6 +40,7 @@ import { getMetamaskProvider, getOkxWalletProvider, getTrustWalletProvider, + getFoxWalletProvider, } from './utils' const evmWallets = [ @@ -359,6 +360,8 @@ export class EvmWallet ? await getOkxWalletProvider() : this.wallet === Wallet.TrustWallet ? await getTrustWalletProvider() + : this.wallet === Wallet.FoxWallet + ? await getFoxWalletProvider() : undefined if (!provider) { diff --git a/packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts b/packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts new file mode 100644 index 000000000..8982b91d2 --- /dev/null +++ b/packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts @@ -0,0 +1,64 @@ +import { isServerSide } from '@injectivelabs/sdk-ts' +import { + BrowserEip1993Provider, + WindowWithEip1193Provider, +} from '@injectivelabs/wallet-base' + +const $window = (isServerSide() + ? {} + : window) as unknown as WindowWithEip1193Provider + +export async function getFoxWalletProvider({ timeout } = { timeout: 3000 }) { + const provider = getFoxWalletFromWindow() + + if (provider) { + return provider + } + + return listenForFoxWalletInitialized({ + timeout, + }) as Promise +} + +async function listenForFoxWalletInitialized({ timeout } = { timeout: 3000 }) { + return new Promise((resolve) => { + const handleInitialization = () => { + resolve(getFoxWalletFromWindow()) + } + + $window.addEventListener('foxwallet#initialized', handleInitialization, { + once: true, + }) + + setTimeout(() => { + $window.removeEventListener('foxwallet#initialized', handleInitialization) + resolve(null) + }, timeout) + }) +} + +function getFoxWalletFromWindow() { + const injectedProviderExist = + typeof window !== 'undefined' && + (typeof $window.ethereum !== 'undefined' || + typeof $window.foxwallet !== 'undefined') + + // No injected providers exist. + if (!injectedProviderExist) { + return + } + + if ($window.foxwallet) { + return $window.foxwallet + } + + if ($window.ethereum.isFoxWallet) { + return $window.ethereum + } + + if ($window.providers) { + return $window.providers.find((p) => p.isFoxWallet) + } + + return +} diff --git a/packages/wallets/wallet-evm/src/strategy/utils/index.ts b/packages/wallets/wallet-evm/src/strategy/utils/index.ts index 2850f5a70..d9806c35b 100644 --- a/packages/wallets/wallet-evm/src/strategy/utils/index.ts +++ b/packages/wallets/wallet-evm/src/strategy/utils/index.ts @@ -3,3 +3,4 @@ export { getBitGetProvider } from './bitget' export { getPhantomProvider } from './phantom' export { getMetamaskProvider } from './metamask' export { getTrustWalletProvider } from './trustWallet' +export { getFoxWalletProvider } from './foxwallet' diff --git a/packages/wallets/wallet-strategy/src/strategy/WalletStrategy.ts b/packages/wallets/wallet-strategy/src/strategy/WalletStrategy.ts index 013e6bbbb..5e4274b99 100644 --- a/packages/wallets/wallet-strategy/src/strategy/WalletStrategy.ts +++ b/packages/wallets/wallet-strategy/src/strategy/WalletStrategy.ts @@ -90,6 +90,11 @@ const createStrategy = ({ ...ethWalletArgs, wallet: Wallet.BitGet, }) + case Wallet.FoxWallet: + return new EvmWalletStrategy({ + ...ethWalletArgs, + wallet: Wallet.FoxWallet, + }) case Wallet.WalletConnect: return new WalletConnectStrategy({ ...ethWalletArgs, From cad430c3276e61f482b04d3c09aebb261022964f Mon Sep 17 00:00:00 2001 From: ljunb Date: Mon, 18 Nov 2024 10:53:58 +0800 Subject: [PATCH 2/3] fixed: get ethereum provider from foxwallet --- .../strategies/wallet-strategy/strategies/FoxWallet/utils.ts | 4 ++-- packages/wallet-ts/src/strategies/wallet-strategy/types.ts | 2 +- packages/wallets/wallet-base/src/types/provider.ts | 2 +- packages/wallets/wallet-evm/src/strategy/strategy.ts | 1 + packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts | 4 ++-- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/utils.ts b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/utils.ts index c1a2fed44..158a774fb 100644 --- a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/utils.ts +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/utils.ts @@ -50,8 +50,8 @@ function getFoxWalletFromWindow() { return } - if ($window.foxwallet) { - return $window.foxwallet + if ($window.foxwallet?.ethereum) { + return $window.foxwallet.ethereum } if ($window.ethereum.isFoxWallet) { diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/types.ts b/packages/wallet-ts/src/strategies/wallet-strategy/types.ts index 4da9754d5..62360e88f 100644 --- a/packages/wallet-ts/src/strategies/wallet-strategy/types.ts +++ b/packages/wallet-ts/src/strategies/wallet-strategy/types.ts @@ -19,7 +19,7 @@ export interface WindowWithEip1193Provider extends Window { providers: BrowserEip1993Provider[] trustWallet?: BrowserEip1993Provider phantom?: { ethereum?: BrowserEip1993Provider } - foxwallet?: BrowserEip1993Provider + foxwallet?: { ethereum?: BrowserEip1993Provider } } export interface WindowWithLedgerSupport extends Window { diff --git a/packages/wallets/wallet-base/src/types/provider.ts b/packages/wallets/wallet-base/src/types/provider.ts index 474181277..cd6aa341c 100644 --- a/packages/wallets/wallet-base/src/types/provider.ts +++ b/packages/wallets/wallet-base/src/types/provider.ts @@ -17,5 +17,5 @@ export interface WindowWithEip1193Provider extends Window { providers: BrowserEip1993Provider[] trustWallet?: BrowserEip1993Provider phantom?: { ethereum?: BrowserEip1993Provider } - foxwallet?: BrowserEip1993Provider + foxwallet?: { ethereum?: BrowserEip1993Provider } } diff --git a/packages/wallets/wallet-evm/src/strategy/strategy.ts b/packages/wallets/wallet-evm/src/strategy/strategy.ts index eaa0150df..fd7d34910 100644 --- a/packages/wallets/wallet-evm/src/strategy/strategy.ts +++ b/packages/wallets/wallet-evm/src/strategy/strategy.ts @@ -49,6 +49,7 @@ const evmWallets = [ Wallet.Metamask, Wallet.OkxWallet, Wallet.TrustWallet, + Wallet.FoxWallet, ] export class EvmWallet diff --git a/packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts b/packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts index 8982b91d2..d3565250f 100644 --- a/packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts +++ b/packages/wallets/wallet-evm/src/strategy/utils/foxwallet.ts @@ -48,8 +48,8 @@ function getFoxWalletFromWindow() { return } - if ($window.foxwallet) { - return $window.foxwallet + if ($window.foxwallet?.ethereum) { + return $window.foxwallet?.ethereum } if ($window.ethereum.isFoxWallet) { From eba97cb3e345bfcd38e55f33093754e9986a7fbd Mon Sep 17 00:00:00 2001 From: ljunb Date: Mon, 18 Nov 2024 11:14:36 +0800 Subject: [PATCH 3/3] dev: remove unsupported event listeners --- .../strategies/FoxWallet/index.ts | 41 +++++++++++-------- .../strategies/FoxWalletCosmos/index.ts | 37 +++++------------ 2 files changed, 34 insertions(+), 44 deletions(-) diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts index 9c95fccb0..22377f7a1 100644 --- a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts @@ -16,7 +16,11 @@ import { } from '../../../types' import { BrowserEip1993Provider, SendTransactionOptions } from '../../types' import BaseConcreteStrategy from './../Base' -import { WalletAction, WalletDeviceType } from '../../../../types/enums' +import { + WalletAction, + WalletDeviceType, + WalletEventListener, +} from '../../../../types/enums' import { getFoxWalletProvider } from './utils' export default class FoxWallet @@ -35,6 +39,19 @@ export default class FoxWallet return Promise.resolve(true) } + public async disconnect() { + if (this.listeners[WalletEventListener.ChainIdChange]) { + const ethereum = await this.getEthereum() + + ethereum.removeListener( + 'chainChanged', + this.listeners[WalletEventListener.ChainIdChange], + ) + } + + this.listeners = {} + } + async getAddresses(): Promise { const ethereum = await this.getEthereum() @@ -241,24 +258,14 @@ export default class FoxWallet ) } - onChainIdChanged(_callback: () => void): void { - // - } - - onAccountChange(_callback: (account: AccountAddress) => void): void { - // - } - - cancelOnChainIdChange(): void { - // - } + async onChainIdChanged(callback: (chain: string) => void): Promise { + const ethereum = await this.getEthereum() - cancelOnAccountChange(): void { - // - } + this.listeners = { + [WalletEventListener.ChainIdChange]: callback, + } - cancelAllEvents(): void { - // + ethereum.on('chainChanged', callback) } private async getEthereum(): Promise { diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts index aaea1d84a..b09a4f7af 100644 --- a/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts @@ -21,11 +21,7 @@ import { import type { DirectSignResponse } from '@cosmjs/proto-signing' import { ConcreteWalletStrategy } from '../../../types' import BaseConcreteStrategy from '../Base' -import { - WalletAction, - WalletDeviceType, - WalletEventListener, -} from '../../../../types/enums' +import { WalletAction, WalletDeviceType } from '../../../../types/enums' import { SendTransactionOptions } from '../../types' import { createCosmosSignDocFromSignDoc } from '../../../../utils/cosmos' import { FoxWallet } from './foxwallet' @@ -56,14 +52,7 @@ export default class FoxWalletCosmos } public async disconnect() { - if (this.listeners[WalletEventListener.AccountChange]) { - window.removeEventListener( - 'fox_keystorechange', - this.listeners[WalletEventListener.AccountChange], - ) - } - - this.listeners = {} + // } async getAddresses(): Promise { @@ -244,20 +233,14 @@ export default class FoxWalletCosmos return Buffer.from(key.pubKey).toString('base64') } - async onAccountChange( - callback: (account: AccountAddress) => void, - ): Promise { - const listener = async () => { - const [account] = await this.getAddresses() - - callback(account) - } - - this.listeners = { - [WalletEventListener.AccountChange]: listener, - } - - window.addEventListener('fox_keystorechange', listener) + onAccountChange(_callback: (account: AccountAddress) => void): Promise { + throw new CosmosWalletException( + new Error('onAccountChange is not supported on FoxWallet'), + { + code: UnspecifiedErrorCode, + context: WalletAction.GetAccounts, + }, + ) } private getFoxWallet(): FoxWallet {