diff --git a/app/context/TokensContext.tsx b/app/context/TokensContext.tsx index fad0885c..3c02ad1b 100644 --- a/app/context/TokensContext.tsx +++ b/app/context/TokensContext.tsx @@ -68,6 +68,22 @@ export function TokensProvider({ children }: { children: ReactNode }) { if (!hasUSDT) { newTokens["Base"].push(usdtBase); } + + // Ensure native ETH is present in the target network + // const nativeETH = { + // name: "Ethereum", + // symbol: "ETH", + // decimals: 18, + // address: "", // Native token has no contract address + // imageUrl: "/logos/eth-logo.svg", + // isNative: true, + // }; + // const hasNativeETH = newTokens["Ethereum"].some( + // (token) => token.symbol === "ETH" && token.isNative, + // ); + // if (!hasNativeETH) { + // newTokens["Ethereum"].push(nativeETH); + // } } // Merge fallback tokens for any networks missing from API response diff --git a/app/hooks/useSmartWalletTransfer.ts b/app/hooks/useSmartWalletTransfer.ts index fd36d6ec..227f68fb 100644 --- a/app/hooks/useSmartWalletTransfer.ts +++ b/app/hooks/useSmartWalletTransfer.ts @@ -5,7 +5,7 @@ import { getExplorerLink } from "../utils"; import { saveTransaction } from "../api/aggregator"; import { trackEvent } from "./analytics/useMixpanel"; import type { Token, Network } from "../types"; -import type { User } from "@privy-io/react-auth"; +import { useSendTransaction, type User } from "@privy-io/react-auth"; interface SmartWalletClient { sendTransaction: (args: { @@ -104,6 +104,45 @@ export function useSmartWalletTransfer({ const tokenData = availableTokens.find( (t) => t.symbol.toUpperCase() === searchToken, ); + + // Native token transfer logic (ETH, BNB, etc.) + if (tokenData?.isNative && tokenData?.address === "") { + const value = BigInt(Math.floor(amount * 1e18)); + const hash = await client?.sendTransaction({ + to: recipientAddress as `0x${string}`, + value, + data: "0x" as `0x${string}`, + }); + if (!hash) throw new Error("No transaction hash returned"); + const txhash = hash as unknown as string; + setTxHash(txhash); + setTransferAmount(amount.toString()); + setTransferToken(token); + setIsSuccess(true); + setIsLoading(false); + toast.success( + `${amount.toString()} ${token} successfully transferred`, + ); + trackEvent("Transfer completed", { + Amount: amount, + "Send token": token, + "Recipient address": recipientAddress, + Network: selectedNetwork.chain.name, + "Transaction hash": hash, + "Transfer date": new Date().toISOString(), + }); + await saveTransferTransaction({ + txHash: txhash, + recipientAddress, + amount, + token, + }); + if (resetForm) resetForm(); + if (refreshBalance) refreshBalance(); + return; + } + + // ERC-20 token transfer logic const tokenAddress = tokenData?.address as `0x${string}` | undefined; const tokenDecimals = tokenData?.decimals; if (!tokenAddress || tokenDecimals === undefined) { @@ -143,7 +182,7 @@ export function useSmartWalletTransfer({ setIsSuccess(true); setIsLoading(false); toast.success(`${amount.toString()} ${token} successfully transferred`); - + // Track successful transfer trackEvent("Transfer completed", { Amount: amount, @@ -153,7 +192,7 @@ export function useSmartWalletTransfer({ "Transaction hash": hash, "Transfer date": new Date().toISOString(), }); - + // Save to transaction history await saveTransferTransaction({ txHash: hash, @@ -164,10 +203,11 @@ export function useSmartWalletTransfer({ if (resetForm) resetForm(); if (refreshBalance) refreshBalance(); } catch (e: unknown) { - const errorMessage = (e as { shortMessage?: string; message?: string }).shortMessage || + const errorMessage = + (e as { shortMessage?: string; message?: string }).shortMessage || (e as { message?: string }).message || "Transfer failed"; - + setError(errorMessage); setIsLoading(false); setIsSuccess(false); @@ -180,9 +220,11 @@ export function useSmartWalletTransfer({ Network: selectedNetwork.chain.name, "Reason for failure": errorMessage, "Transfer date": new Date().toISOString(), - "Error type": errorMessage.includes("429") ? "RPC Rate Limited" : - errorMessage.includes("HTTP") ? "RPC Connection Error" : - "Transaction Error", + "Error type": errorMessage.includes("429") + ? "RPC Rate Limited" + : errorMessage.includes("HTTP") + ? "RPC Connection Error" + : "Transaction Error", }); } }, diff --git a/app/mocks.ts b/app/mocks.ts index d80a3aa4..8cd7ac0f 100644 --- a/app/mocks.ts +++ b/app/mocks.ts @@ -1,6 +1,5 @@ import { arbitrum, base, bsc, polygon, lisk, celo, mainnet } from "viem/chains"; - export const acceptedCurrencies = [ { name: "NGN", @@ -18,7 +17,7 @@ export const acceptedCurrencies = [ name: "TZS", label: "Tanzanian Shilling (TZS)", }, - { + { name: "MWK", label: "Malawian Kwacha (MWK)", disabled: true, @@ -60,7 +59,7 @@ export const networks = [ }, { chain: mainnet, - imageUrl: "/logos/ethereum-logo.svg", + imageUrl: "/logos/eth-logo.svg", }, { chain: lisk, @@ -73,10 +72,10 @@ export const networks = [ chain: polygon, imageUrl: "/logos/polygon-logo.svg", }, -// { -// chain: hedera, -// imageUrl: "/logos/hedera-logo.svg", -// }, + // { + // chain: hedera, + // imageUrl: "/logos/hedera-logo.svg", + // }, // { // chain: scroll, // imageUrl: "/logos/scroll-logo.svg", diff --git a/app/types.ts b/app/types.ts index 98bf0761..cbe08264 100644 --- a/app/types.ts +++ b/app/types.ts @@ -212,6 +212,7 @@ export type Token = { decimals: number; address: string; imageUrl?: string; + isNative?: boolean; }; export type APIToken = { diff --git a/app/utils.ts b/app/utils.ts index c106aa7f..a2942205 100644 --- a/app/utils.ts +++ b/app/utils.ts @@ -255,6 +255,14 @@ export const FALLBACK_TOKENS: { [key: string]: Token[] } = { address: "0x46c85152bfe9f96829aa94755d9f915f9b10ef5f", imageUrl: "/logos/cngn-logo.svg", }, + { + name: "Ethereum", + symbol: "ETH", + decimals: 18, + address: "", // Native token has no contract address + imageUrl: "/logos/eth-logo.svg", + isNative: true, + }, ], "Arbitrum One": [ { @@ -344,28 +352,28 @@ export const FALLBACK_TOKENS: { [key: string]: Token[] } = { }, ], Ethereum: [ - { - name: "USD Coin", - symbol: "USDC", - decimals: 6, - address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - imageUrl: "/logos/usdc-logo.svg", - }, - { - name: "Tether USD", - symbol: "USDT", - decimals: 6, - address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", - imageUrl: "/logos/usdt-logo.svg", - }, - { + { + name: "USD Coin", + symbol: "USDC", + decimals: 6, + address: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + imageUrl: "/logos/usdc-logo.svg", + }, + { + name: "Tether USD", + symbol: "USDT", + decimals: 6, + address: "0xdAC17F958D2ee523a2206206994597C13D831ec7", + imageUrl: "/logos/usdt-logo.svg", + }, + { name: "cNGN", symbol: "cNGN", decimals: 6, address: "0x17CDB2a01e7a34CbB3DD4b83260B05d0274C8dab", imageUrl: "/logos/cngn-logo.svg", }, -], + ], }; /** @@ -428,6 +436,22 @@ export async function getNetworkTokens(network = ""): Promise { if (!hasUSDT) { tokens["Base"].push(usdtBase); } + + // Ensure native ETH is present in the target network + // const nativeETH = { + // name: "Ethereum", + // symbol: "ETH", + // decimals: 18, + // address: "", // Native token has no contract address + // imageUrl: "/logos/eth-logo.svg", + // isNative: true, + // }; + // const hasNativeETH = tokens["Ethereum"].some( + // (token) => token.symbol === "ETH" && token.isNative, + // ); + // if (!hasNativeETH) { + // tokens["Ethereum"].push(nativeETH); + // } } // Merge fallback tokens for any networks missing from API response Object.keys(FALLBACK_TOKENS).forEach((networkName) => { @@ -471,16 +495,25 @@ export async function fetchWalletBalance( // Fetch balances in parallel const balancePromises = supportedTokens.map(async (token: Token) => { try { - const balanceInWei = await client.readContract({ - address: token.address as `0x${string}`, - abi: erc20Abi, - functionName: "balanceOf", - args: [address as `0x${string}`], - }); - const balance = Number(balanceInWei) / Math.pow(10, token.decimals); - // Ensure balance is a valid number - balances[token.symbol] = isNaN(balance) ? 0 : balance; - return balances[token.symbol]; + if (token.isNative && token.address === "") { + // Native token balance (ETH, BNB, etc.) + const balanceInWei = await client.getBalance({ address }); + const balance = Number(balanceInWei) / Math.pow(10, token.decimals); + balances[token.symbol] = isNaN(balance) ? 0 : balance; + return balances[token.symbol]; + } else { + // ERC-20 token balance + const balanceInWei = await client.readContract({ + address: token.address as `0x${string}`, + abi: erc20Abi, + functionName: "balanceOf", + args: [address as `0x${string}`], + }); + const balance = Number(balanceInWei) / Math.pow(10, token.decimals); + // Ensure balance is a valid number + balances[token.symbol] = isNaN(balance) ? 0 : balance; + return balances[token.symbol]; + } } catch (error) { console.error(`Error fetching balance for ${token.symbol}:`, error); balances[token.symbol] = 0; @@ -609,7 +642,7 @@ export function getGatewayContractAddress(network = ""): string | undefined { Optimism: "0xd293fcd3dbc025603911853d893a4724cf9f70a0", Celo: "0xf418217e3f81092ef44b81c5c8336e6a6fdb0e4b", Lisk: "0xff0E00E0110C1FBb5315D276243497b66D3a4d8a", - Ethereum: "0x8d2c0d398832b814e3814802ff2dc8b8ef4381e5" + Ethereum: "0x8d2c0d398832b814e3814802ff2dc8b8ef4381e5", }[network]; } @@ -1205,7 +1238,8 @@ export function calculateSenderFee( const defaultFeePercent = 0.1; // 0.1% default fee for local transfers const maxFeeCapInHumanReadable = 10000; // 10k CNGN cap in human-readable units const decimalsMultiplier = BigInt(10 ** tokenDecimals); - const maxFeeCapInBaseUnits = BigInt(maxFeeCapInHumanReadable) * decimalsMultiplier; // 10k CNGN in base units + const maxFeeCapInBaseUnits = + BigInt(maxFeeCapInHumanReadable) * decimalsMultiplier; // 10k CNGN in base units // Calculate fee in human-readable format const calculatedFee = isLocalTransfer diff --git a/package.json b/package.json index 5e51d249..8e939269 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,8 @@ "typescript": "^5.8.3" }, "engines": { - "node": "22.x" + "node": "22.x", + "pnpm": "10.14.0" }, "packageManager": "pnpm@10.14.0", "pnpm": { diff --git a/public/logos/eth-logo.svg b/public/logos/eth-logo.svg new file mode 100644 index 00000000..30d2142c --- /dev/null +++ b/public/logos/eth-logo.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + +