Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support FoxWallet #514

Open
wants to merge 4 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<WalletDeviceType> {
return Promise.resolve(WalletDeviceType.Browser)
}

async enable(): Promise<boolean> {
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<string[]> {
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<string> {
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<string> {
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,
})
}
}
Comment on lines +80 to +98
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Specify the type of the transaction parameter

The transaction parameter is currently typed as unknown. Providing a specific type will improve type safety and code readability.

Please specify the appropriate type for transaction. For example:

-  async sendEthereumTransaction(
-    transaction: unknown,
+  async sendEthereumTransaction(
+    transaction: EthereumTransactionRequest,

Replace EthereumTransactionRequest with the actual type that matches the expected transaction object.

Committable suggestion skipped: line range outside the PR's diff.


async sendTransaction(
transaction: TxRaw,
options: SendTransactionOptions,
): Promise<TxResponse> {
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<string> {
return this.signEip712TypedData(eip712json, address)
}

async signEip712TypedData(
eip712json: string,
address: AccountAddress,
): Promise<string> {
const ethereum = await this.getEthereum()

try {
return await ethereum.request({
method: 'eth_signTypedData_v4',
params: [eip712json, address],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Correct parameter order for eth_signTypedData_v4

The parameters for eth_signTypedData_v4 should be [address, typedData], but they are currently [typedData, address], which will cause incorrect behavior.

Apply this diff to fix the parameter order:

         method: 'eth_signTypedData_v4',
-        params: [eip712json, address],
+        params: [address, eip712json],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
params: [eip712json, address],
params: [address, eip712json],

})
} 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<string> {
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<DirectSignResponse> {
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<string> {
const ethereum = await this.getEthereum()

try {
const signature = await ethereum.request({
method: 'personal_sign',
params: [toUtf8(data), signer],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Convert data to a hex string for personal_sign

The personal_sign method expects the message parameter to be a hex-encoded string. Currently, toUtf8(data) returns a Uint8Array. You should convert it to a hex string prefixed with 0x.

Apply this diff to convert the data to a hex string:

-        params: [toUtf8(data), signer],
+        params: [`0x${Buffer.from(data).toString('hex')}`, signer],

Ensure that data is properly converted to a hex string.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
params: [toUtf8(data), signer],
params: [`0x${Buffer.from(data).toString('hex')}`, 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<string> {
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,
})
}
Comment on lines +217 to +222
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use appropriate exception type for Ethereum-related errors

In Ethereum-related methods, wrapping errors in CosmosWalletException might be misleading. Consider using a more appropriate exception type like WalletException to accurately reflect the context.

Update the exception to use WalletException:

-      throw new CosmosWalletException(new Error((e as any).message), {
+      throw new WalletException(new Error((e as any).message), {

Apply similar changes in other Ethereum-related methods where CosmosWalletException is used.

Committable suggestion skipped: line range outside the PR's diff.

}

async getEthereumTransactionReceipt(txHash: string): Promise<string> {
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
}
Comment on lines +229 to +241
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid potential stack overflow due to recursive retry logic

The transactionReceiptRetry function recursively calls itself when receipt is not found, which could lead to a stack overflow after many retries.

Refactor the function to use a loop instead of recursion:

    const transactionReceiptRetry = async () => {
-     const receipt = await ethereum.request({
-       method: 'eth_getTransactionReceipt',
-       params: [txHash],
-     })
-
-     if (!receipt) {
-       await sleep(interval)
-       await transactionReceiptRetry()
-     }
-
-     return receipt
+     let receipt = null
+     while (!receipt) {
+       receipt = await ethereum.request({
+         method: 'eth_getTransactionReceipt',
+         params: [txHash],
+       })
+       if (!receipt) {
+         await sleep(interval)
+       }
+     }
+     return receipt
    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const transactionReceiptRetry = async () => {
const receipt = await ethereum.request({
method: 'eth_getTransactionReceipt',
params: [txHash],
})
if (!receipt) {
await sleep(interval)
await transactionReceiptRetry()
}
return receipt
}
const transactionReceiptRetry = async () => {
let receipt = null
while (!receipt) {
receipt = await ethereum.request({
method: 'eth_getTransactionReceipt',
params: [txHash],
})
if (!receipt) {
await sleep(interval)
}
}
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<string> {
throw new WalletException(
new Error('You can only fetch PubKey from Cosmos native wallets'),
)
}

async onChainIdChanged(callback: (chain: string) => void): Promise<void> {
const ethereum = await this.getEthereum()

this.listeners = {
[WalletEventListener.ChainIdChange]: callback,
}

ethereum.on('chainChanged', callback)
}

private async getEthereum(): Promise<BrowserEip1993Provider> {
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
}
}
Original file line number Diff line number Diff line change
@@ -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<BrowserEip1993Provider>
}

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)
})
}
Comment on lines +20 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Improve error handling and prevent potential memory leaks

The current implementation has several potential issues:

  1. No error handling for invalid event data
  2. Possible memory leak if timeout occurs after event resolution
  3. No Promise rejection for error cases
 async function listenForFoxWalletInitialized(
   { timeout } = { timeout: 3000 },
 ) {
-  return new Promise((resolve) => {
+  return new Promise((resolve, reject) => {
+    let isResolved = false;
     const handleInitialization = () => {
-      resolve(getFoxWalletFromWindow())
+      if (isResolved) return;
+      isResolved = true;
+      try {
+        const provider = getFoxWalletFromWindow();
+        resolve(provider);
+      } catch (error) {
+        reject(error);
+      }
     }

     $window.addEventListener('foxwallet#initialized', handleInitialization, {
       once: true,
     })

-    setTimeout(() => {
+    const timeoutId = setTimeout(() => {
+      if (isResolved) return;
+      isResolved = true;
       $window.removeEventListener(
         'foxwallet#initialized',
         handleInitialization,
       )
       resolve(null)
     }, timeout)
   })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)
})
}
async function listenForFoxWalletInitialized(
{ timeout } = { timeout: 3000 },
) {
return new Promise((resolve, reject) => {
let isResolved = false;
const handleInitialization = () => {
if (isResolved) return;
isResolved = true;
try {
const provider = getFoxWalletFromWindow();
resolve(provider);
} catch (error) {
reject(error);
}
}
$window.addEventListener('foxwallet#initialized', handleInitialization, {
once: true,
})
const timeoutId = setTimeout(() => {
if (isResolved) return;
isResolved = true;
$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
}
Comment on lines +42 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Enhance type safety and return consistency

The function could benefit from improved type safety and consistent return types.

-function getFoxWalletFromWindow() {
+function getFoxWalletFromWindow(): BrowserEip1993Provider | undefined {
   const injectedProviderExist =
-    typeof window !== 'undefined' &&
-    (typeof $window.ethereum !== '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) {
+  if (Array.isArray($window.providers)) {
     return $window.providers.find((p) => p.isFoxWallet)
   }

   return
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
}
function getFoxWalletFromWindow(): BrowserEip1993Provider | undefined {
const injectedProviderExist =
(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 (Array.isArray($window.providers)) {
return $window.providers.find((p) => p.isFoxWallet)
}
return
}

Loading