diff --git a/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts b/packages/wallet-ts/src/strategies/wallet-strategy/WalletStrategy.ts index 27be93207..d732ca077 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.js' import LedgerLive from './strategies/Ledger/LedgerLive.js' import LedgerLegacy from './strategies/Ledger/LedgerLegacy.js' import Magic from './strategies/Magic.js' +import FoxWallet from './strategies/FoxWallet/index.js' +import FoxWalletCosmos from './strategies/FoxWalletCosmos/index.js' import { isEthWallet, isCosmosWallet } from './utils.js' import { Wallet, WalletDeviceType } from '../../types/enums.js' import { MagicMetadata, SendTransactionOptions } from './types.js' @@ -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..22377f7a1 --- /dev/null +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWallet/index.ts @@ -0,0 +1,287 @@ +/* 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, + WalletEventListener, +} 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) + } + + 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() + + 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'), + ) + } + + async onChainIdChanged(callback: (chain: string) => void): Promise { + const ethereum = await this.getEthereum() + + this.listeners = { + [WalletEventListener.ChainIdChange]: callback, + } + + ethereum.on('chainChanged', callback) + } + + 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..158a774fb --- /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?.ethereum) { + return $window.foxwallet.ethereum + } + + 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..b09a4f7af --- /dev/null +++ b/packages/wallet-ts/src/strategies/wallet-strategy/strategies/FoxWalletCosmos/index.ts @@ -0,0 +1,262 @@ +/* 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 } 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() { + // + } + + 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') + } + + 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 { + 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..62360e88f 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?: { ethereum?: 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 bbdacf4c0..6864b8995 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..cd6aa341c 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?: { ethereum?: BrowserEip1993Provider } } diff --git a/packages/wallets/wallet-base/src/utils/wallet.ts b/packages/wallets/wallet-base/src/utils/wallet.ts index 3daff13cb..73b8e2d52 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 3af529d7d..4377a0044 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/index.js' const evmWallets = [ @@ -48,6 +49,7 @@ const evmWallets = [ Wallet.Metamask, Wallet.OkxWallet, Wallet.TrustWallet, + Wallet.FoxWallet, ] export class EvmWallet @@ -359,6 +361,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..d3565250f --- /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?.ethereum) { + return $window.foxwallet?.ethereum + } + + 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 cce87fa81..785b1a265 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.js' export { getPhantomProvider } from './phantom.js' export { getMetamaskProvider } from './metamask.js' export { getTrustWalletProvider } from './trustWallet.js' +export { getFoxWalletProvider } from './foxwallet.js' 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,