diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index a3309f0d..608509bc 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1564,7 +1564,7 @@ "description": "Not connected indicator" }, "not_connected_text": { - "message": "\"Connect / Disconnect\" is an indicator for when your ArConnect wallet is connected to an Arweave or AO application. For regular use like sending, receiving, checking balances, etc., ArConnect will always show disconnected.", + "message": "\"Connect / Disconnect\" is a label for when your ArConnect wallet is connected to an Arweave or AO dapp. For regular use like sending, receiving, checking balances, etc., ArConnect will always show disconnected.", "description": "Not connected indicator explainer" }, "disconnect": { @@ -2009,11 +2009,11 @@ "description": "Popup description about ao token transfer learn more" }, "ao_degraded": { - "message": "Unable to connect to AO Token Process", + "message": "Token balance error", "description": "ao degraded title text" }, "ao_degraded_description": { - "message": "AO balance will be available when
network issues are resolved.", + "message": "Oops, ArConnect is unable to fetch your token balance. Please try again later.", "description": "ao degraded description text" }, "network_issue": { diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 199c4db1..0bfc4df4 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -1999,11 +1999,11 @@ "description": "Popup description about ao token transfer learn more" }, "ao_degraded": { - "message": "无法连接到 AO 令牌进程", + "message": "代币余额错误", "description": "ao degraded title text" }, "ao_degraded_description": { - "message": "网络问题解决后,AO 余额将可用。", + "message": "ArConnect 无法获取您的代币余额。这只是一个用户界面问题。请稍后重试。", "description": "ao degraded description text" }, "network_issue": { diff --git a/src/components/popup/Token.tsx b/src/components/popup/Token.tsx index f23afbe2..fd0d6a73 100644 --- a/src/components/popup/Token.tsx +++ b/src/components/popup/Token.tsx @@ -1,8 +1,4 @@ -import { - formatFiatBalance, - formatTokenBalance, - balanceToFractioned -} from "~tokens/currency"; +import { formatFiatBalance, balanceToFractioned } from "~tokens/currency"; import { type MouseEventHandler, useEffect, @@ -23,8 +19,6 @@ import * as viewblock from "~lib/viewblock"; import Squircle from "~components/Squircle"; import useSetting from "~settings/hook"; import styled from "styled-components"; -import Arweave from "arweave"; -import { useGateway } from "~gateways/wayfinder"; import aoLogo from "url:/assets/ecosystem/ao-logo.svg"; import { getUserAvatar } from "~lib/avatar"; import { formatBalance } from "~utils/format"; @@ -34,6 +28,7 @@ import BigNumber from "bignumber.js"; import JSConfetti from "js-confetti"; import browser from "webextension-polyfill"; import { AO_NATIVE_TOKEN } from "~utils/ao_import"; +import { useBalance } from "~wallets/hooks"; export default function Token({ onClick, ...props }: Props) { const ref = useRef(null); @@ -166,13 +161,13 @@ export default function Token({ onClick, ...props }: Props) { {props?.loading ? ( ) : props?.error ? ( - + - + ) : props?.networkError ? ( - + - + ) : ( <> {showTooltip ? ( @@ -373,6 +368,10 @@ const BalanceTooltip = styled(TooltipV2)` margin-right: 1rem; `; +const MessageTooltip = styled(TooltipV2)` + max-width: 290px; +`; + const Image = styled.img` width: 16px; padding: 0 8px; @@ -486,33 +485,23 @@ export function ArToken({ onClick }: ArTokenProps) { }); // load ar balance - const [balance, setBalance] = useState(BigNumber("0")); const [fiatBalance, setFiatBalance] = useState(BigNumber("0")); const [displayBalance, setDisplayBalance] = useState("0"); const [totalBalance, setTotalBalance] = useState(""); const [showTooltip, setShowTooltip] = useState(false); - // memoized requirements to ensure stability - const requirements = useMemo(() => ({ ensureStake: true }), []); - const gateway = useGateway(requirements); + const balance = useBalance(); useEffect(() => { (async () => { if (!activeAddress) return; - const arweave = new Arweave(gateway); - - // fetch balance - const winstonBalance = await arweave.wallets.getBalance(activeAddress); - const arBalance = BigNumber(arweave.ar.winstonToAr(winstonBalance)); - setBalance(arBalance); - - const formattedBalance = formatBalance(arBalance); + const formattedBalance = formatBalance(balance); setTotalBalance(formattedBalance.tooltipBalance); setShowTooltip(formattedBalance.showTooltip); setDisplayBalance(formattedBalance.displayBalance); })(); - }, [activeAddress, gateway]); + }, [activeAddress, balance]); useEffect(() => { setFiatBalance(balance.multipliedBy(price)); diff --git a/src/components/popup/home/Balance.tsx b/src/components/popup/home/Balance.tsx index e8194427..b27543f9 100644 --- a/src/components/popup/home/Balance.tsx +++ b/src/components/popup/home/Balance.tsx @@ -6,7 +6,6 @@ import { Loading, TooltipV2 } from "@arconnect/components"; import { useEffect, useMemo, useState, type HTMLProps } from "react"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; -import { useHistory } from "~utils/hash_router"; import { useBalance } from "~wallets/hooks"; import { getArPrice } from "~lib/coingecko"; import { getAppURL } from "~utils/format"; @@ -25,8 +24,8 @@ import styled from "styled-components"; import Arweave from "arweave"; import { removeDecryptionKey } from "~wallets/auth"; import { findGateway } from "~gateways/wayfinder"; -import type { Gateway } from "~gateways/gateway"; import BigNumber from "bignumber.js"; +import { retryWithDelay, retryWithDelayAndTimeout } from "~utils/retry"; export default function Balance() { const [loading, setLoading] = useState(false); @@ -103,8 +102,7 @@ export default function Balance() { (async () => { if (!activeAddress) return; setLoading(true); - const gateway = await findGateway({ graphql: true }); - const history = await balanceHistory(activeAddress, gateway); + const history = await balanceHistory(activeAddress); setHistoricalBalance(history); setLoading(false); @@ -192,8 +190,9 @@ export default function Balance() { ); } -async function balanceHistory(address: string, gateway: Gateway) { - const arweave = new Arweave(gateway); +async function balanceHistory(address: string) { + const gateway = await findGateway({ graphql: true }); + let arweave = new Arweave(gateway); let minHeight = 0; try { const { height } = await arweave.network.getInfo(); @@ -203,8 +202,9 @@ async function balanceHistory(address: string, gateway: Gateway) { // find txs coming in and going out const inTxs = ( - await gql( - ` + await retryWithDelay(() => + gql( + ` query($recipient: String!, $minHeight: Int!) { transactions(recipients: [$recipient], first: 100, bundledIn: null, block: {min: $minHeight}) { edges { @@ -226,13 +226,15 @@ async function balanceHistory(address: string, gateway: Gateway) { } } `, - { recipient: address, minHeight } + { recipient: address, minHeight } + ) ) ).data.transactions.edges; const outTxs = ( - await gql( - ` + await retryWithDelay(() => + gql( + ` query($owner: String!, $minHeight: Int!) { transactions(owners: [$owner], first: 100, bundledIn: null, block: {min: $minHeight}) { edges { @@ -254,7 +256,8 @@ async function balanceHistory(address: string, gateway: Gateway) { } } `, - { owner: address, minHeight } + { owner: address, minHeight } + ) ) ).data.transactions.edges; @@ -266,9 +269,14 @@ async function balanceHistory(address: string, gateway: Gateway) { .sort((a, b) => b.block.timestamp - a.block.timestamp); // Sort by newest to oldest // Get the current balance - let balance = BigNumber( - arweave.ar.winstonToAr(await arweave.wallets.getBalance(address)) - ); + const winstonBalance = await retryWithDelayAndTimeout(async () => { + const gateway = await findGateway({}); + arweave = new Arweave(gateway); + const balance = await arweave.wallets.getBalance(address); + if (isNaN(+balance)) throw new Error("Balance is invalid"); + return balance; + }); + let balance = BigNumber(arweave.ar.winstonToAr(winstonBalance)); // Initialize the result array with the current balance const res = [balance.toNumber()]; diff --git a/src/components/popup/home/Transactions.tsx b/src/components/popup/home/Transactions.tsx index 6d55b533..8e24e4d7 100644 --- a/src/components/popup/home/Transactions.tsx +++ b/src/components/popup/home/Transactions.tsx @@ -166,14 +166,13 @@ export default function Transactions() { : "Pending"} - {transaction.transactionType !== "printArchive" && ( -
-
{getFormattedAmount(transaction)}
- - {getFormattedFiatAmount(transaction, arPrice, currency)} - -
- )} + +
+
{getFormattedAmount(transaction)}
+ + {getFormattedFiatAmount(transaction, arPrice, currency)} + +
)) diff --git a/src/lib/transactions.ts b/src/lib/transactions.ts index 698a9ad6..7e747f05 100644 --- a/src/lib/transactions.ts +++ b/src/lib/transactions.ts @@ -143,6 +143,8 @@ export const getFormattedAmount = (transaction: ExtendedTransaction) => { }).toFixed()} ${transaction.aoInfo.tickerName}`; } return ""; + case "printArchive": + return `${parseFloat(transaction.node.fee.ar).toFixed(3)} AR`; default: return ""; } @@ -154,11 +156,22 @@ export const getFormattedFiatAmount = ( currency: string ) => { try { - if (transaction.node.quantity) { + if ( + transaction.node.quantity && + transaction.transactionType !== "printArchive" + ) { const fiatBalance = BigNumber(transaction.node.quantity.ar).multipliedBy( arPrice ); return formatFiatBalance(fiatBalance, currency); + } else if ( + transaction.node.fee && + transaction.transactionType === "printArchive" + ) { + const fiatBalance = BigNumber(transaction.node.fee.ar).multipliedBy( + arPrice + ); + return formatFiatBalance(fiatBalance, currency); } } catch {} return ""; diff --git a/src/notifications/api.ts b/src/notifications/api.ts index 5600b72c..81021229 100644 --- a/src/notifications/api.ts +++ b/src/notifications/api.ts @@ -26,6 +26,9 @@ export type RawTransaction = { quantity: { ar: string; }; + fee: { + ar: string; + }; block: { timestamp: number; height: number; diff --git a/src/notifications/utils.ts b/src/notifications/utils.ts index 0a09f4b4..f306a99d 100644 --- a/src/notifications/utils.ts +++ b/src/notifications/utils.ts @@ -258,6 +258,7 @@ query ($address: String!) { recipient owner { address } quantity { ar } + fee { ar } block { timestamp, height } tags { name diff --git a/src/routes/popup/transaction/[id].tsx b/src/routes/popup/transaction/[id].tsx index 9a1e8c25..f70c9c64 100644 --- a/src/routes/popup/transaction/[id].tsx +++ b/src/routes/popup/transaction/[id].tsx @@ -475,6 +475,12 @@ export default function Transaction({ id: rawId, gw, message }: Props) { + + + {browser.i18n.getMessage("transaction_fee")} + + {transaction.fee.ar} AR + {!message && ( diff --git a/src/tokens/aoTokens/sync.ts b/src/tokens/aoTokens/sync.ts index 2607fe4e..11d0a5dc 100644 --- a/src/tokens/aoTokens/sync.ts +++ b/src/tokens/aoTokens/sync.ts @@ -17,6 +17,7 @@ import { Id, Owner } from "./ao"; +import { withRetry } from "~utils/retry"; /** Tokens storage name */ const AO_TOKENS = "ao_tokens"; @@ -36,44 +37,6 @@ const gateway = { protocol: "https" }; -/** - * Generic retry function for any async operation. - * @param fn - The async function to be retried. - * @param maxRetries - Maximum retry attempts. - * @param retryDelay - Delay between retries in milliseconds. - * @returns A promise of the type that the async function returns. - */ -async function withRetry( - fn: () => Promise, - maxRetries: number = 3, - retryDelay: number = 100 -): Promise { - let lastError: any; - - const delay = (ms: number) => - new Promise((resolve) => setTimeout(resolve, ms)); - - for (let attempt = 1; attempt <= maxRetries; attempt++) { - try { - return await fn(); - } catch (error) { - lastError = error; - if (attempt < maxRetries) { - const waitTime = Math.pow(2, attempt - 1) * retryDelay; - console.log(`Attempt ${attempt} failed, retrying in ${waitTime}ms...`); - await delay(waitTime); - } else { - console.error( - `All ${maxRetries} attempts failed. Last error:`, - lastError - ); - } - } - } - - throw lastError; -} - async function getTokenInfo(id: string): Promise { const body = { Id, diff --git a/src/utils/retry.ts b/src/utils/retry.ts index 4c7e48b0..e487e40b 100644 --- a/src/utils/retry.ts +++ b/src/utils/retry.ts @@ -30,3 +30,79 @@ export async function retryWithDelay( return attempt(); } + +/** + * Retries a given asynchronous function up to a maximum number of attempts with a timeout for each attempt. + * @param fn - The asynchronous function to retry, which should return a Promise. + * @param maxAttempts - The maximum number of attempts to make. + * @param delay - The delay between attempts in milliseconds. + * @param timeout - The maximum time to wait for each attempt in milliseconds. + * @return A Promise that resolves with the result of the function or rejects after all attempts fail. + */ +export async function retryWithDelayAndTimeout( + fn: () => Promise, + maxAttempts: number = 3, + delay: number = 1000, + timeout: number = 10000 +): Promise { + for (let attempt = 0; attempt <= maxAttempts; attempt++) { + try { + // Create a race between the function and the timeout + const result = await Promise.race([ + fn(), + new Promise((_, reject) => + setTimeout(() => reject(new Error("Request timed out")), timeout) + ) + ]); + return result; + } catch (error) { + if (attempt < maxAttempts) { + // console.log(`Attempt ${attempt} failed: ${error.message}. Retrying in ${delay}ms...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } else { + throw error; + } + } + } + + // Final fallback error + throw new Error("Max attempts reached without success."); +} + +/** + * Generic retry function for any async operation. + * @param fn - The async function to be retried. + * @param maxRetries - Maximum retry attempts. + * @param retryDelay - Delay between retries in milliseconds. + * @returns A promise of the type that the async function returns. + */ +export async function withRetry( + fn: () => Promise, + maxRetries: number = 3, + retryDelay: number = 100 +): Promise { + let lastError: any; + + const delay = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error; + if (attempt < maxRetries) { + const waitTime = Math.pow(2, attempt - 1) * retryDelay; + console.log(`Attempt ${attempt} failed, retrying in ${waitTime}ms...`); + await delay(waitTime); + } else { + console.error( + `All ${maxRetries} attempts failed. Last error:`, + lastError + ); + } + } + } + + throw lastError; +} diff --git a/src/wallets/hooks.ts b/src/wallets/hooks.ts index 6bafbcd5..67ee3ade 100644 --- a/src/wallets/hooks.ts +++ b/src/wallets/hooks.ts @@ -1,7 +1,7 @@ import type { WalletInterface } from "~components/welcome/load/Migrate"; import type { JWKInterface } from "arweave/web/lib/wallet"; import { type AnsUser, getAnsProfile } from "~lib/ans"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useStorage } from "@plasmohq/storage/hook"; import { defaultGateway } from "~gateways/gateway"; import { ExtensionStorage } from "~utils/storage"; @@ -10,6 +10,7 @@ import type { HardwareApi } from "./hardware"; import type { StoredWallet } from "~wallets"; import Arweave from "arweave"; import BigNumber from "bignumber.js"; +import { retryWithDelayAndTimeout } from "~utils/retry"; /** * Wallets with details hook @@ -113,19 +114,31 @@ export function useBalance() { // balance in AR const [balance, setBalance] = useState(BigNumber("0")); - useEffect(() => { - (async () => { - if (!activeAddress) return; - - const gateway = await findGateway({}); - const arweave = new Arweave(gateway); + const fetchBalance = useCallback(async () => { + if (!activeAddress) { + setBalance(BigNumber("0")); + return; + } + + const gateway = await findGateway({}); + const arweave = new Arweave(gateway); + + // fetch balance + const winstonBalance = await arweave.wallets.getBalance(activeAddress); + if (isNaN(+winstonBalance)) { + throw new Error("Invalid balance returned"); + } + const arBalance = BigNumber(arweave.ar.winstonToAr(winstonBalance)); + setBalance(arBalance); + }, [activeAddress]); - // fetch balance - const winstonBalance = await arweave.wallets.getBalance(activeAddress); + useEffect(() => { + if (!activeAddress) return; - setBalance(BigNumber(arweave.ar.winstonToAr(winstonBalance))); - })(); - }, [activeAddress]); + retryWithDelayAndTimeout(fetchBalance).catch((error) => { + console.log(`Error fetching balance: ${error}`); + }); + }, [activeAddress, fetchBalance]); return balance; }