diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 99c64745..452fae32 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -61,6 +61,10 @@ "message": "Save", "description": "Save button text" }, + "generate": { + "message": "Generate", + "description": "Generate button text" + }, "add_wallet": { "message": "Add wallet", "description": "Add a wallet text" @@ -299,6 +303,10 @@ "message": "Allows using dispatch transactions.", "description": "Description for the \"DISPATCH\" permission" }, + "permissionAccessTokens": { + "message": "Allows access to view tokens and their balances that you've added to your wallet", + "description": "Description for the permission to access user-added token and token balances" + }, "copyId": { "message": "Copy ID", "description": "Copy ID button text" @@ -1373,6 +1381,14 @@ "message": "Generate QR code", "description": "Generate QR code button" }, + "generate_qr_code_title": { + "message": "Enter your password to generate QR Code", + "description": "Generate QR code title" + }, + "cannot_generate_qr_code": { + "message": "Cannot generate QR code for your hardware wallet.", + "description": "Cannot generate QR text" + }, "viewblock": { "message": "Viewblock", "description": "view block" @@ -2179,5 +2195,9 @@ "mismatch_warning": { "message": "Your wallet has a mismatched bit length. You can proceed, but may encounter errors. Transactions could fail or behave unexpectedly.", "description": "warning for mismatch wallet" + }, + "transak_unavailable": { + "message": "Transak is unavailable at the moment. Please try again later.", + "description": "Transak unavailable error message" } } diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index 76aed103..b2c6e7c3 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -61,6 +61,10 @@ "message": "节省", "description": "Save button text" }, + "generate": { + "message": "产生", + "description": "Generate button text" + }, "add_wallet": { "message": "添加钱包", "description": "Add a wallet text" @@ -295,6 +299,10 @@ "message": "允许使用调度交易。", "description": "Description for the \"DISPATCH\" permission" }, + "permissionAccessTokens": { + "message": "允许查看您已添加到钱包中的代币及其余额", + "description": "Description for the permission to access user-added token and token balances" + }, "copyId": { "message": "复制 ID", "description": "Copy ID button text" @@ -1361,6 +1369,14 @@ "message": "生成二维码", "description": "Generate QR code button" }, + "generate_qr_code_title": { + "message": "输入密码生成二维码", + "description": "Generate QR code title" + }, + "cannot_generate_qr_code": { + "message": "无法为您的硬件钱包生成二维码。", + "description": "Cannot generate QR text" + }, "viewblock": { "message": "Viewblock", "description": "view block" @@ -2165,5 +2181,9 @@ "mismatch_warning": { "message": "您的钱包存在不匹配的位长度。您可以继续操作,但可能会遇到错误。交易可能会失败或表现异常。", "description": "warning for mismatch wallet" + }, + "transak_unavailable": { + "message": "Transak 目前不可用。请稍后再试。", + "description": "Transak unavailable error message" } } diff --git a/assets/ecosystem/aolink.svg b/assets/ecosystem/aolink.svg new file mode 100644 index 00000000..f28746ca --- /dev/null +++ b/assets/ecosystem/aolink.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ecosystem/arswap.png b/assets/ecosystem/arswap.png new file mode 100644 index 00000000..075707ff Binary files /dev/null and b/assets/ecosystem/arswap.png differ diff --git a/assets/ecosystem/autonomous-dca-agent.png b/assets/ecosystem/autonomous-dca-agent.png new file mode 100644 index 00000000..387a281b Binary files /dev/null and b/assets/ecosystem/autonomous-dca-agent.png differ diff --git a/assets/ecosystem/betteridea.png b/assets/ecosystem/betteridea.png new file mode 100644 index 00000000..b7f4a35c Binary files /dev/null and b/assets/ecosystem/betteridea.png differ diff --git a/assets/ecosystem/dexi.svg b/assets/ecosystem/dexi.svg new file mode 100644 index 00000000..4112322d --- /dev/null +++ b/assets/ecosystem/dexi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/ecosystem/liquidops.svg b/assets/ecosystem/liquidops.svg new file mode 100644 index 00000000..614c4e21 --- /dev/null +++ b/assets/ecosystem/liquidops.svg @@ -0,0 +1,23 @@ + + + + + + + + + diff --git a/assets/ecosystem/llama.png b/assets/ecosystem/llama.png new file mode 100644 index 00000000..977878b7 Binary files /dev/null and b/assets/ecosystem/llama.png differ diff --git a/package.json b/package.json index bebd756d..3333d10b 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "plimit-lit": "^3.0.1", "pretty-bytes": "^6.0.0", "qrcode.react": "^3.1.0", + "qrloop": "^1.4.1", "react": "18.2.0", "react-dom": "18.2.0", "react-fast-marquee": "^1.3.5", diff --git a/src/api/background.ts b/src/api/background.ts index 5f2b1ebc..9980ce8f 100644 --- a/src/api/background.ts +++ b/src/api/background.ts @@ -41,6 +41,8 @@ import signDataItemModule from "./modules/sign_data_item"; import signDataItem from "./modules/sign_data_item/sign_data_item.background"; import subscriptionModule from "./modules/subscription"; import subscription from "./modules/subscription/subscription.background"; +import userTokensModule from "./modules/user_tokens"; +import userTokens from "./modules/user_tokens/user_tokens.background"; /** Background modules */ const modules: BackgroundModule[] = [ @@ -63,7 +65,8 @@ const modules: BackgroundModule[] = [ { ...privateHashModule, function: privateHash }, { ...verifyMessageModule, function: verifyMessage }, { ...signDataItemModule, function: signDataItem }, - { ...subscriptionModule, function: subscription } + { ...subscriptionModule, function: subscription }, + { ...userTokensModule, function: userTokens } ]; export default modules; diff --git a/src/api/foreground.ts b/src/api/foreground.ts index eac7e96a..8b32738a 100644 --- a/src/api/foreground.ts +++ b/src/api/foreground.ts @@ -59,6 +59,8 @@ import signDataItemModule from "./modules/sign_data_item"; import signDataItem, { finalizer as signDataItemFinalizer } from "./modules/sign_data_item/sign_data_item.foreground"; +import userTokensModule from "./modules/user_tokens"; +import userTokens from "./modules/user_tokens/user_tokens.foreground"; /** Foreground modules */ const modules: ForegroundModule[] = [ @@ -93,7 +95,8 @@ const modules: ForegroundModule[] = [ function: signDataItem, finalizer: signDataItemFinalizer }, - { ...subscriptionModule, function: subscription } + { ...subscriptionModule, function: subscription }, + { ...userTokensModule, function: userTokens } ]; export default modules; diff --git a/src/api/modules/user_tokens/index.ts b/src/api/modules/user_tokens/index.ts new file mode 100644 index 00000000..9fc69959 --- /dev/null +++ b/src/api/modules/user_tokens/index.ts @@ -0,0 +1,11 @@ +import type { PermissionType } from "~applications/permissions"; +import type { ModuleProperties } from "~api/module"; + +const permissions: PermissionType[] = ["ACCESS_TOKENS"]; + +const userTokensModule: ModuleProperties = { + functionName: "userTokens", + permissions +}; + +export default userTokensModule; diff --git a/src/api/modules/user_tokens/user_tokens.background.ts b/src/api/modules/user_tokens/user_tokens.background.ts new file mode 100644 index 00000000..1b138990 --- /dev/null +++ b/src/api/modules/user_tokens/user_tokens.background.ts @@ -0,0 +1,50 @@ +import type { ModuleFunction } from "~api/background"; +import { ExtensionStorage } from "~utils/storage"; +import { + getAoTokenBalance, + getNativeTokenBalance, + type TokenInfo, + type TokenInfoWithBalance +} from "~tokens/aoTokens/ao"; +import { AO_NATIVE_TOKEN } from "~utils/ao_import"; + +const background: ModuleFunction = async ( + _, + options?: { fetchBalance?: boolean } +) => { + const address = await ExtensionStorage.get("active_address"); + const tokens = (await ExtensionStorage.get("ao_tokens")) || []; + + if (!options?.fetchBalance) { + return tokens; + } + + const enrichedTokens: TokenInfoWithBalance[] = await Promise.all( + tokens.map(async (token) => { + let balance: string | null = null; + + try { + if (token.processId === AO_NATIVE_TOKEN) { + balance = await getNativeTokenBalance(address); + } else { + const balanceResult = await getAoTokenBalance( + address, + token.processId + ); + balance = balanceResult.toString(); + } + } catch (error) { + console.error( + `Error fetching balance for token ${token.Name} (${token.processId}):`, + error + ); + } + + return { ...token, balance }; + }) + ); + + return enrichedTokens; +}; + +export default background; diff --git a/src/api/modules/user_tokens/user_tokens.foreground.ts b/src/api/modules/user_tokens/user_tokens.foreground.ts new file mode 100644 index 00000000..220afbdb --- /dev/null +++ b/src/api/modules/user_tokens/user_tokens.foreground.ts @@ -0,0 +1,6 @@ +import type { ModuleFunction } from "~api/module"; + +// no need to transform anything in the foreground +const foreground: ModuleFunction = () => {}; + +export default foreground; diff --git a/src/applications/permissions.ts b/src/applications/permissions.ts index 5ee8ca8b..095dd724 100644 --- a/src/applications/permissions.ts +++ b/src/applications/permissions.ts @@ -10,7 +10,8 @@ export type PermissionType = | "DECRYPT" | "SIGNATURE" | "ACCESS_ARWEAVE_CONFIG" - | "DISPATCH"; + | "DISPATCH" + | "ACCESS_TOKENS"; /** * All permissions with their descriptions @@ -24,7 +25,8 @@ export const permissionData: Record = { DECRYPT: "permissionDescriptionDecrypt", SIGNATURE: "permissionDescriptionSignature", ACCESS_ARWEAVE_CONFIG: "permissionDescriptionArweaveConfig", - DISPATCH: "permissionDescriptionDispatch" + DISPATCH: "permissionDescriptionDispatch", + ACCESS_TOKENS: "permissionAccessTokens" }; /** diff --git a/src/routes/popup/explore.tsx b/src/routes/popup/explore.tsx index 79ac47e9..c2616c05 100644 --- a/src/routes/popup/explore.tsx +++ b/src/routes/popup/explore.tsx @@ -37,27 +37,29 @@ export default function Explore() {
{filteredApps.map((app, index) => ( - - { - browser.tabs.create({ url: app.links.website }); - }} - > - - - - - - <AppTitle>{app.name}</AppTitle> - <Pill>{app.category}</Pill> - - {app.description} - + + + { + browser.tabs.create({ url: app.links.website }); + }} + > + + + + + + <AppTitle>{app.name}</AppTitle> + <Pill>{app.category}</Pill> + + {app.description} + + ` diff --git a/src/routes/popup/purchase.tsx b/src/routes/popup/purchase.tsx index 53ec090d..7be8c2a9 100644 --- a/src/routes/popup/purchase.tsx +++ b/src/routes/popup/purchase.tsx @@ -4,7 +4,8 @@ import { Text, ListItem, ButtonV2, - Loading + Loading, + useToasts } from "@arconnect/components"; import browser from "webextension-polyfill"; import { ChevronRight } from "@untitled-ui/icons-react"; @@ -19,6 +20,7 @@ import type { PaymentType, Quote } from "~lib/onramper"; import { useHistory } from "~utils/hash_router"; import { ExtensionStorage } from "~utils/storage"; import { useDebounce } from "~wallets/hooks"; +import { retryWithDelay } from "~utils/retry"; export default function Purchase() { const [push] = useHistory(); @@ -32,6 +34,7 @@ export default function Purchase() { const [showPaymentSelector, setShowPaymentSelector] = useState(false); const [showCurrencySelector, setShowCurrencySelector] = useState(false); const [quote, setQuote] = useState(); + const { setToast } = useToasts(); const handlePaymentClose = () => { setShowPaymentSelector(false); @@ -41,6 +44,19 @@ export default function Purchase() { setShowCurrencySelector(false); }; + const showTransakErrorToast = () => { + setToast({ + type: "error", + content: browser.i18n.getMessage("transak_unavailable"), + duration: 2400 + }); + }; + + const finishUp = (quote: Quote | null) => { + setQuote(quote); + setLoading(false); + }; + //segment useEffect(() => { trackPage(PageType.TRANSAK_PURCHASE); @@ -84,8 +100,7 @@ export default function Purchase() { !selectedCurrency || !paymentMethod ) { - setLoading(false); - setQuote(null); + finishUp(null); return; } const baseUrl = "https://api.transak.com/api/v1/pricing/public/quotes"; @@ -106,18 +121,31 @@ export default function Purchase() { const url = `${baseUrl}?${params.toString()}`; try { - const response = await fetch(url); + const response = await retryWithDelay(() => fetch(url)); if (!response.ok) { - setQuote(null); - throw new Error("Network response was not ok"); + try { + const resJson = await response.json(); + if (resJson?.error?.message) { + setToast({ + type: "error", + content: resJson?.error?.message, + duration: 2400 + }); + } else { + throw new Error("Network response was not ok"); + } + } catch { + showTransakErrorToast(); + } + finishUp(null); + return; } const data = await response.json(); - setQuote(data.response); - setLoading(false); + finishUp(data.response); } catch (error) { console.error("Error fetching data:", error); - setQuote(null); - setLoading(false); + showTransakErrorToast(); + finishUp(null); } setLoading(false); }; diff --git a/src/routes/popup/receive.tsx b/src/routes/popup/receive.tsx index 081192fa..07f9a111 100644 --- a/src/routes/popup/receive.tsx +++ b/src/routes/popup/receive.tsx @@ -115,26 +115,26 @@ export default function Receive({ walletName, walletAddress }: ReceiveProps) { ); } -const Wrapper = styled.div` +export const Wrapper = styled.div` display: flex; flex-direction: column; height: calc(100vh - 72px); `; -const ContentWrapper = styled.div` +export const ContentWrapper = styled.div` display: flex; flex-direction: column; justify-content: center; `; -const AddressField = styled(ButtonV2)` +export const AddressField = styled(ButtonV2)` display: flex; align-items: center; gap: 5px; font-weight: 500; `; -const CopyAction = styled(CopyIcon)` +export const CopyAction = styled(CopyIcon)` font-size: 1.25rem; width: 1em; height: 1em; @@ -151,7 +151,7 @@ const CopyAction = styled(CopyIcon)` } `; -const QRCodeWrapper = styled.div` +export const QRCodeWrapper = styled.div` display: flex; justify-content: center; align-items: center; diff --git a/src/routes/popup/settings/wallets/[address]/qr.tsx b/src/routes/popup/settings/wallets/[address]/qr.tsx index 0bd00f90..1fb0fcd5 100644 --- a/src/routes/popup/settings/wallets/[address]/qr.tsx +++ b/src/routes/popup/settings/wallets/[address]/qr.tsx @@ -1,26 +1,230 @@ -import { useStorage } from "@plasmohq/storage/hook"; -import { useMemo } from "react"; -import Receive from "~routes/popup/receive"; -import { ExtensionStorage } from "~utils/storage"; -import type { StoredWallet } from "~wallets"; +import { + useToasts, + Section, + TooltipV2, + useInput, + ButtonV2, + InputV2, + Spacer, + Text +} from "@arconnect/components"; +import { CheckIcon, CopyIcon } from "@iconicicons/react"; +import copy from "copy-to-clipboard"; +import { QRCodeSVG } from "qrcode.react"; +import { + useEffect, + useRef, + useState, + type Key, + type MouseEventHandler +} from "react"; +import { useLocation } from "wouter"; +import HeadV2 from "~components/popup/HeadV2"; +import { WarningIcon } from "~components/popup/Token"; +import browser from "webextension-polyfill"; +import { Degraded, WarningWrapper } from "~routes/popup/send"; +import { formatAddress } from "~utils/format"; +import { getKeyfile, type DecryptedWallet } from "~wallets"; +import { freeDecryptedWallet } from "~wallets/encryption"; +import { + AddressField, + ContentWrapper, + CopyAction, + QRCodeWrapper, + Wrapper +} from "~routes/popup/receive"; +import { dataToFrames } from "qrloop"; +import { checkPassword } from "~wallets/auth"; export default function GenerateQR({ address }: { address: string }) { - // wallets - const [wallets] = useStorage( - { - key: "wallets", - instance: ExtensionStorage - }, - [] - ); + const [wallet, setWallet] = useState(null); + const [copied, setCopied] = useState(false); + const [loading, setLoading] = useState(false); + const [frames, setFrames] = useState([]); - // this wallet - const wallet = useMemo( - () => wallets?.find((w) => w.address === address), - [wallets, address] - ); + const [, setLocation] = useLocation(); + const { setToast } = useToasts(); + const passwordInput = useInput(); + + const isHardware = wallet?.type === "hardware"; + + const copyAddress: MouseEventHandler = (e) => { + e.stopPropagation(); + copy(address); + setCopied(true); + setTimeout(() => setCopied(false), 1000); + setToast({ + type: "success", + duration: 2000, + content: `${formatAddress(address, 3)} ${browser.i18n.getMessage( + "copied_address_2" + )}` + }); + }; - if (!wallet) return <>; + async function generateQr() { + try { + setLoading(true); + const isPasswordCorrect = await checkPassword(passwordInput.state); + if (isPasswordCorrect) { + const wallet = await getKeyfile(address); + setWallet(wallet); + } else { + passwordInput.setStatus("error"); + setToast({ + type: "error", + content: browser.i18n.getMessage("invalidPassword"), + duration: 2200 + }); + } + } catch { + } finally { + setLoading(false); + } + } - return ; + useEffect(() => { + if ((wallet as any)?.keyfile) { + setFrames(dataToFrames(JSON.stringify((wallet as any)?.keyfile))); + freeDecryptedWallet((wallet as any).keyfile); + } + }, [wallet]); + + useEffect(() => { + return () => setFrames([]); + }, []); + + return ( + +
+ { + if (address) { + setLocation(`/quick-settings/wallets/${address}`); + } else { + setLocation("/"); + } + }} + /> +
+ {wallet ? ( +
+ {isHardware ? ( + + + + +
+ + {browser.i18n.getMessage("cannot_generate_qr_code")} + +
+
+ ) : ( + +
+ + + +
+
+ + {formatAddress(address ?? "", 6)} + + + + +
+
+ )} +
+ ) : ( +
+ + {browser.i18n.getMessage("generate_qr_code_title")} + + { + if (e.key !== "Enter") return; + generateQr(); + }} + /> + + + {browser.i18n.getMessage("generate")} + +
+ )} +
+ ); } + +const QRCodeLoop = ({ + frames, + size, + fps +}: { + frames: string[]; + size: number; + fps: number; +}) => { + const [frame, setFrame] = useState(0); + const rafRef = useRef(null); + + useEffect(() => { + const nextFrame = (frame: number, frames: string[]) => { + frame = (frame + 1) % frames.length; + return frame; + }; + + let lastT: number; + const loop = (t: number) => { + rafRef.current = requestAnimationFrame(loop); + if (!lastT) lastT = t; + if ((t - lastT) * fps < 1000) return; + lastT = t; + setFrame((prevFrame) => nextFrame(prevFrame, frames)); + }; + rafRef.current = requestAnimationFrame(loop); + + return () => { + cancelAnimationFrame(rafRef.current); + }; + }, [frames, fps]); + + return ( +
+ {frames.map((chunk: any, i: Key) => ( +
+ +
+ ))} +
+ ); +}; diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index 53e7b50a..3d25085e 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -163,21 +163,8 @@ export function useAoTokens( const balance = await timeoutPromise( (async () => { if (id === AO_NATIVE_TOKEN) { - const res = await dryrun({ - Id, - Owner: activeAddress, - process: AO_NATIVE_TOKEN_BALANCE_MIRROR, - tags: [{ name: "Action", value: "Balance" }] - }); - const balance = res.Messages[0].Data; - if (balance) { - return new Quantity( - BigInt(balance), - BigInt(12) - ).toString(); - } - // default return - return new Quantity(0, BigInt(12)).toString(); + const res = await getNativeTokenBalance(activeAddress); + return res; } else { let balance: string; if (refresh) { @@ -253,6 +240,17 @@ export async function getAoTokenBalance( } } +export async function getNativeTokenBalance(address: string): Promise { + const res = await dryrun({ + Id, + Owner: address, + process: AO_NATIVE_TOKEN_BALANCE_MIRROR, + tags: [{ name: "Action", value: "Balance" }] + }); + const balance = res.Messages[0].Data; + return balance ? new Quantity(BigInt(balance), BigInt(12)).toString() : "0"; +} + export function useAoTokensCache(): [TokenInfoWithBalance[], boolean] { const [balances, setBalances] = useState<{ id: string; balance: string }[]>( [] @@ -547,6 +545,6 @@ export interface TokenInfo { export type TokenInfoWithProcessId = TokenInfo & { processId: string }; export interface TokenInfoWithBalance extends TokenInfo { - id: string; + id?: string; balance: string; } diff --git a/src/utils/apps.ts b/src/utils/apps.ts index ac9f14fd..42a877a9 100644 --- a/src/utils/apps.ts +++ b/src/utils/apps.ts @@ -5,16 +5,17 @@ import arwikiLogo from "url:/assets/ecosystem/arwiki.png"; import bazarLogo from "url:/assets/ecosystem/bazar.png"; import protocollandLogo from "url:/assets/ecosystem/protocolland.svg"; import permaswapLogo from "url:/assets/ecosystem/permaswap.svg"; -import pianityLogo from "url:/assets/ecosystem/pianity.png"; import barkLogo from "url:/assets/ecosystem/bark.png"; -import ansLogo from "url:/assets/ecosystem/ans-logo.svg"; import arnsLogo from "url:/assets/ecosystem/arns.svg"; import astroLogo from "url:/assets/ecosystem/astro.png"; -import artByCityLogo from "url:/assets/ecosystem/artbycity.png"; import permapagesLogo from "url:/assets/ecosystem/permapages.svg"; -import echoLogo from "url:/assets/ecosystem/echo.svg"; -import permaFacts from "url:/assets/ecosystem/permafacts.svg"; -import arLogoLight from "url:/assets/ar/logo_light.png"; +import dexiLogo from "url:/assets/ecosystem/dexi.svg"; +import dcaAgentLogo from "url:/assets/ecosystem/autonomous-dca-agent.png"; +import aoLinkLogo from "url:/assets/ecosystem/aolink.svg"; +import llamaLogo from "url:/assets/ecosystem/llama.png"; +import arswapLogo from "url:/assets/ecosystem/arswap.png"; +import liquidopsLogo from "url:/assets/ecosystem/liquidops.svg"; +import betterideaLogo from "url:/assets/ecosystem/betteridea.png"; export interface App { name: string; @@ -38,8 +39,7 @@ export const apps: App[] = [ { name: "Bark", category: "Exchange", - description: - "Bark is the AO Computer's first decentralized Finance. It supports AMM trading pairs and extreme scalability.", + description: "Bark is the AO Computer's first decentralized exchange.", assets: { logo: barkLogo, thumbnail: "/apps/bark/thumbnail.png", @@ -54,7 +54,7 @@ export const apps: App[] = [ name: "Protocol.Land", category: "Storage", description: - "Code collaboration, reimagined. Protocol.Land is a decentralized, source controlled, code collaboration where you own your code.", + "Protocol.Land is a decentralized home for decentralized codebases.", assets: { logo: protocollandLogo, thumbnail: "/apps/protocolland/thumbnail.png", @@ -70,9 +70,9 @@ export const apps: App[] = [ }, { name: "Astro", - category: "De-fi", + category: "Defi", description: - "Astro USD (USDA) is the very first stablecoin in the Arweave (and AO Computer) ecosystem.", + "Astro introduces USDA as the first overcollateralized stablecoin on AO.", assets: { logo: astroLogo, thumbnail: "/apps/astro/thumbnail.png", @@ -86,18 +86,19 @@ export const apps: App[] = [ } }, { - name: "AFTR Market", - category: "Developer Tooling", + name: "LiquidOps", + category: "Defi", description: - "AFTR Market provides asset management and governance on-chain for Arweave assets.", + "A simple, secure lending & borrowing platform for AR & AO assets.", assets: { - logo: aftrmarketLogo, - thumbnail: "/apps/aftr/thumbnail.png" + logo: liquidopsLogo, + thumbnail: "/apps/astro/thumbnail.png", + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" }, links: { - website: "https://www.aftr.market/", - twitter: "https://twitter.com/AftrMarket", - discord: "https://discord.gg/YEy8VpuNXR" + website: "https://www.liquidops.io", + twitter: "https://x.com/Liquid_Ops" } }, { @@ -107,7 +108,8 @@ export const apps: App[] = [ "The first fully decentralized atomic asset exchange built on the permaweb. Through the power of the Universal Content Marketplace (UCM) protocol and the Universal Data License (UDL) content creators can trade digital assets with real world rights.", assets: { logo: bazarLogo, - thumbnail: "/apps/bazar/thumbnail.gif" + thumbnail: "/apps/bazar/thumbnail.gif", + lightBackground: "rgba(230, 235, 240, 1)" }, links: { website: "https://bazar.arweave.dev", @@ -115,99 +117,112 @@ export const apps: App[] = [ } }, { - name: "Arweave Name System", - category: "Social", + name: "AFTR Market", + category: "Infrastructure", description: - "The Arweave Name System (ArNS) works similarly to traditional Domain Name Services - but with ArNS, the registry is decentralized, permanent, and stored on Arweave. It's a simple way to name and help you - and your users - find your data, apps, or websites on Arweave.", + "AFTR Market provides asset management and governance on-chain for Arweave assets.", assets: { - logo: arnsLogo, - thumbnail: "/apps/arns/thumbnail.jpeg" + logo: aftrmarketLogo, + thumbnail: "/apps/aftr/thumbnail.png" }, links: { - website: "https://arns.app", - twitter: "https://twitter.com/ar_io_network", - discord: "https://discord.com/invite/HGG52EtTc2", - github: "https://github.com/ar-io" + website: "https://www.aftr.market/", + twitter: "https://twitter.com/AftrMarket", + discord: "https://discord.gg/YEy8VpuNXR" } }, { - name: "Art By City", - category: "Publishing", + name: "Dexi", + category: "Defi", description: - "Art By City is a chain-agnostic Web3 art and creative content protocol built on Arweave. The protocol is governed by the Art By City DAO. The Art By City DAO is a Profit Sharing Community or PSC. The Art By City community governs development of the Art By City protocol, dApps, and tools artists will need to take control of their Web3 experience.", + "Dexi autonomously identifies, collects, and aggregates financial data from events within the AO network, including asset prices, token swaps, liquidity fluctuations, and token asset characteristics.", assets: { - logo: artByCityLogo, - thumbnail: "/apps/artbycity/thumbnail.png", - lightBackground: "rgba(255, 255, 255, 1)", - darkBackground: "rgba(255, 255, 255, 1)" + logo: dexiLogo, + thumbnail: "/apps/dexi/thumbnail.png", + lightBackground: "rgba(230, 235, 240, 1)" }, links: { - website: "https://artby.city", - twitter: "https://twitter.com/artbycity", - discord: "https://discord.gg/w4Yhc95b8p", - github: "https://github.com/art-by-city" + website: "https://dexi.arweave.dev/", + twitter: "https://x.com/autonomous_af" } }, { - name: "ECHO", - category: "Social", + name: "Autonomous DCA Agent", + category: "Agent", description: - "ECHO is the first decentralized social engagement protocol based on Arweave. Its goal is to provide the fundamental infrastructure of Web3 social by introducing the first comment widget that can be deployed on any Web3 website with permanent data storage, so that users can speak up for themselves in a decentralized, permissionless, and censorship-resistant environment. ", + "The Autonomous DCA Agent executes a dynamic dollar-cost-average (DCA) investment strategy across various liquidity pools within the AO ecosystem.", assets: { - logo: echoLogo, - thumbnail: "/apps/echo/thumbnail.png" + logo: dcaAgentLogo, + thumbnail: "/apps/autonomousdca/thumbnail.png", + lightBackground: "rgba(230, 235, 240, 1)" }, links: { - website: "https://0xecho.com", - twitter: "https://twitter.com/0x_ECHO", - discord: "https://discord.gg/KFxyaw9Wdj", - github: "https://github.com/0x-echo" + website: "https://dca_agent.arweave.dev/", + twitter: "https://x.com/autonomous_af" } }, { - name: "Permafacts", - category: "Publishing", + name: "ao Link", + category: "Developer Tooling", + description: + "ao.link serves as a message explorer for the ao Network, offering functionalities similar to block explorers in conventional blockchain systems.", + assets: { + logo: aoLinkLogo, + thumbnail: "/apps/aolink/logo.png", + lightBackground: "rgba(230, 235, 240, 1)" + }, + links: { + website: "https://www.ao.link/", + twitter: "https://x.com/TheDataOS" + } + }, + { + name: "Llama Land", + category: "Social", description: - "A provably neutral publishing platform, built on top of the #FactsProtocol, aimed at dis-intermediating the truth. Publish assertions, and take your position in the Fact Marketplace!", + "AI powered MMO game built on AO. Petition the Llama King for Llama Coin! 100% onchain.", assets: { - logo: permaFacts, - thumbnail: "/apps/permafacts/thumbnail.png" + logo: llamaLogo, + thumbnail: "/apps/llamaland/logo.png", + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" }, links: { - website: "https://permafacts.arweave.dev", - twitter: "https://twitter.com/permafacts", - discord: "https://discord.gg/uGg8VAvqU7", - github: "https://github.com/facts-laboratory" + website: "https://llamaland.g8way.io/#/", + twitter: "https://x.com/LlamaLandAO" } }, { - name: "Pianity", - category: "NFTs", + name: "ArSwap", + category: "Exchange", description: - "Pianity is a music NFT platform – built on environmentally-conscious Arweave technology — where musicians and their community gather to create, sell, buy and collect songs in limited editions. Pianity's pioneering approach, which includes free listening for all, enables deeper connections between artists and their audience.", + "Unlocking DeFi on AO. Swap tokens, provide liquidity, and earn fees.", assets: { - logo: pianityLogo, - thumbnail: "/apps/pianity/thumbnail.png" + logo: arswapLogo, + thumbnail: "/apps/arswap/logo.png", + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" }, links: { - website: "https://pianity.com", - twitter: "https://twitter.com/pianitynft", - discord: "https://discord.gg/pianity" + website: "https://arswap.org/", + twitter: "https://x.com/ar_swap" } }, { - name: "Arweave Name Service (ANS)", + name: "Arweave Name System", category: "Social", description: - "ans.gg is a popular name service built on top of the Arweave blockchain. Buy your domain once, own forever.", + "The Arweave Name System (ArNS) works similarly to traditional Domain Name Services - but with ArNS, the registry is decentralized, permanent, and stored on Arweave. It's a simple way to name and help you - and your users - find your data, apps, or websites on Arweave.", assets: { - logo: ansLogo, - thumbnail: "/apps/ans/thumbnail.png" + logo: arnsLogo, + thumbnail: "/apps/arns/thumbnail.jpeg", + lightBackground: "rgba(230, 235, 240, 1)" }, links: { - website: "https://ans.gg", - twitter: "https://twitter.com/ArweaveANS", - discord: "https://discord.gg/decentland" + website: "https://arns.app", + twitter: "https://twitter.com/ar_io_network", + discord: "https://discord.com/invite/HGG52EtTc2", + github: "https://github.com/ar-io" } }, { @@ -217,7 +232,9 @@ export const apps: App[] = [ "Create and manage your own permanent web3 profile and permaweb pages built on Arweave.", assets: { logo: permapagesLogo, - thumbnail: "/apps/permapages/thumbnail.png" + thumbnail: "/apps/permapages/thumbnail.png", + lightBackground: "rgba(230, 235, 240, 1)", + darkBackground: "rgba(19, 28, 37, 1)" }, links: { website: "https://permapages.app", @@ -243,17 +260,20 @@ export const apps: App[] = [ } }, { - name: "ArCode Studio", - category: "Development", - description: - "ArCode Studio is an online IDE for smartweave contracts. As ArCode works on the browser all the files are saved in cache memory and removed when the cache is cleared.", + name: "BetterIDEa IDE", + category: "Developer Tooling", + description: "Feature rich web IDE for building on AO", assets: { - logo: arLogoLight, - thumbnail: "/apps/arcode/thumbnail.jpeg" + logo: betterideaLogo, + thumbnail: "/apps/betteridea/thumbnail.png", + lightBackground: "rgba(240, 240, 240, 1)", + darkBackground: "rgba(20, 34, 19, 1)" }, links: { - website: "https://arcode.ar-io.dev", - github: "https://github.com/luckyr13/arcode" + website: "https://betteridea.dev", + twitter: "https://twitter.com/betteridea_dev", + discord: "https://discord.gg/nm6VKUQBrA", + github: "https://github.com/betteridea-dev" } }, { @@ -263,7 +283,8 @@ export const apps: App[] = [ "As MediaWiki is the software that powers Wikipedia, ArWiki is the software that powers the Arweave Wiki. However, ArWiki is a Web3 platform -- it is completely decentralized, and is hosted on and served from the Arweave permaweb itself.", assets: { logo: arwikiLogo, - thumbnail: "/apps/arwiki/thumbnail.jpeg" + thumbnail: "/apps/arwiki/thumbnail.jpeg", + lightBackground: "rgba(230, 235, 240, 1)" }, links: { website: "https://arwiki.wiki", @@ -277,7 +298,8 @@ export const apps: App[] = [ "A decentralized archival platform that preserves human history and culture digitally.", assets: { logo: alexLogo, - thumbnail: "/apps/alex/thumbnail.png" + thumbnail: "/apps/alex/thumbnail.png", + lightBackground: "rgba(230, 235, 240, 1)" }, links: { website: "https://alex.arweave.dev/", @@ -293,7 +315,7 @@ export const apps: App[] = [ assets: { logo: permaswapLogo, thumbnail: permaswapLogo, - lightBackground: "rgba(230, 235, 240, 1)", + // lightBackground: "rgba(230, 235, 240, 1)", darkBackground: "rgba(19, 28, 37, 1)" }, links: { diff --git a/src/utils/retry.ts b/src/utils/retry.ts new file mode 100644 index 00000000..4c7e48b0 --- /dev/null +++ b/src/utils/retry.ts @@ -0,0 +1,32 @@ +/** + * Retries a given function up to a maximum number of attempts. + * @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. + * @return A Promise that resolves with the result of the function or rejects after all attempts fail. + */ +export async function retryWithDelay( + fn: () => Promise, + maxAttempts: number = 3, + delay: number = 1000 +): Promise { + let attempts = 0; + + const attempt = async (): Promise => { + try { + return await fn(); + } catch (error) { + attempts += 1; + if (attempts < maxAttempts) { + // console.log(`Attempt ${attempts} failed, retrying...`) + return new Promise((resolve) => + setTimeout(() => resolve(attempt()), delay) + ); + } else { + throw error; + } + } + }; + + return attempt(); +} diff --git a/src/wallets/index.ts b/src/wallets/index.ts index abcd442c..d572f8ee 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -208,6 +208,54 @@ export async function getActiveKeyfile(): Promise { return decryptedWallet; } +/** + * Get the wallet with decrypted JWK + * + * !!IMPORTANT!! + * + * When using this function, always make sure to remove the keyfile + * from the memory, after it is no longer needed, using the + * "freeDecryptedWallet(activekeyfile.keyfile)" function. + * + * @returns wallet with decrypted JWK + */ +export async function getKeyfile(address: string): Promise { + // fetch data from storage + const wallets = await getWallets(); + const wallet = wallets.find((wallet) => wallet.address === address); + + // return if hardware wallet + if (wallet.type === "hardware") { + return wallet; + } + + // get decryption key + let decryptionKey = await getDecryptionKey(); + + // unlock ArConnect if the decryption key is undefined + // this means that the user has to enter their decryption + // key so it can be used later + if (!decryptionKey && !!wallet) { + await authenticate({ + type: "unlock" + }); + + // re-read the decryption key + decryptionKey = await getDecryptionKey(); + } + + // decrypt keyfile + const decryptedKeyfile = await decryptWallet(wallet.keyfile, decryptionKey); + + // construct decrypted wallet object + const decryptedWallet: DecryptedWallet = { + ...wallet, + keyfile: decryptedKeyfile + }; + + return decryptedWallet; +} + /** * Add a wallet for the user * diff --git a/yarn.lock b/yarn.lock index 99d7b467..d888d11d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5428,6 +5428,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA== + check-password-strength@^2.0.7: version "2.0.10" resolved "https://registry.yarnpkg.com/check-password-strength/-/check-password-strength-2.0.10.tgz#d716d767944f43aa83665cdf96a2c28e18b2a7b6" @@ -5882,6 +5887,11 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +crypt@0.0.2: + version "0.0.2" + resolved "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow== + crypto-browserify@^3.12.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -7591,6 +7601,11 @@ is-buffer@^2.0.5: resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== +is-buffer@~1.1.6: + version "1.1.6" + resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== + is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -8638,6 +8653,15 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +md5@^2.3.0: + version "2.3.0" + resolved "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f" + integrity sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g== + dependencies: + charenc "0.0.2" + crypt "0.0.2" + is-buffer "~1.1.6" + mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -10089,6 +10113,14 @@ qrcode.react@^1.0.1: prop-types "^15.6.0" qr.js "0.0.0" +qrloop@^1.4.1: + version "1.4.1" + resolved "https://registry.npmjs.org/qrloop/-/qrloop-1.4.1.tgz#bfe90e6aeb4c4735edec13d14681edc56e79e29d" + integrity sha512-LXkwCl1Qd8imTHb+KqjMn+cHmuncyFT81AXoooWJvbG3+g9q61l9udSRPgY4cgl+5goQHuAK4teEdUF6UErYXw== + dependencies: + buffer "^6.0.3" + md5 "^2.3.0" + qs@^6.11.2: version "6.12.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.12.1.tgz#39422111ca7cbdb70425541cba20c7d7b216599a"