From 02872b6f9ee12a4ade099d4037fb97a5aa8921fe Mon Sep 17 00:00:00 2001 From: elehmandevelopment Date: Wed, 10 Apr 2024 14:11:41 -0600 Subject: [PATCH 01/19] fix: abbreviate large numbers --- src/components/popup/Token.tsx | 16 +++++++++++----- src/utils/format.ts | 31 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/components/popup/Token.tsx b/src/components/popup/Token.tsx index ab676131a..df2a25dd3 100644 --- a/src/components/popup/Token.tsx +++ b/src/components/popup/Token.tsx @@ -22,6 +22,7 @@ import { defaultGateway } from "~gateways/gateway"; import { useGateway } from "~gateways/wayfinder"; import aoLogo from "url:/assets/ecosystem/ao-logo.svg"; import { getUserAvatar } from "~lib/avatar"; +import { abbreviateNumber } from "~utils/format"; export default function Token({ onClick, ...props }: Props) { // display theme @@ -38,17 +39,18 @@ export default function Token({ onClick, ...props }: Props) { [props] ); - const balance = useMemo( - () => formatTokenBalance(fractBalance), - [fractBalance] - ); + const balance = useMemo(() => { + const formattedBalance = formatTokenBalance(fractBalance); + const numBalance = parseFloat(formattedBalance.replace(/,/g, "")); + return abbreviateNumber(numBalance); + }, [fractBalance]); // token price const { price, currency } = usePrice(props.ticker); // fiat balance const fiatBalance = useMemo(() => { - if (!price) return "--"; + if (!price) return
; const estimate = fractBalance * price; @@ -71,6 +73,10 @@ export default function Token({ onClick, ...props }: Props) { })(); }, [props, theme, logo]); + useEffect(() => { + console.log("balance:", balance); + }, [balance]); + return ( diff --git a/src/utils/format.ts b/src/utils/format.ts index ab75af63a..ad467b7c7 100644 --- a/src/utils/format.ts +++ b/src/utils/format.ts @@ -73,3 +73,34 @@ export const formatSettingName = (name: string) => { .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(" "); }; + +/** + * Abbreviates large numbers into a more readable format, using M, B, and T for millions, billions, and trillions respectively. + * + * @param value The numeric value to abbreviate, as a number or string. + * @returns The abbreviated string representation of the number. + */ +export function abbreviateNumber(value: number): string { + let suffix = ""; + let abbreviatedValue = value; + + if (value >= 1e12) { + // Trillions + suffix = "T"; + abbreviatedValue = value / 1e12; + } else if (value >= 1e9) { + // Billions + suffix = "B"; + abbreviatedValue = value / 1e9; + } else if (value >= 1e6) { + // Millions + suffix = "M"; + abbreviatedValue = value / 1e6; + } + + if (abbreviatedValue % 1 === 0) { + return `${abbreviatedValue}${suffix}`; + } else { + return `${abbreviatedValue.toFixed(1)}${suffix}`; + } +} From b5e0928a2ae84c0841371a06641cf475c86b2ce0 Mon Sep 17 00:00:00 2001 From: elehmandevelopment Date: Fri, 12 Apr 2024 15:01:45 -0600 Subject: [PATCH 02/19] ci: add tooltip to token balances --- src/components/popup/Token.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/popup/Token.tsx b/src/components/popup/Token.tsx index df2a25dd3..e9bdb2add 100644 --- a/src/components/popup/Token.tsx +++ b/src/components/popup/Token.tsx @@ -8,7 +8,7 @@ import { hoverEffect, useTheme } from "~utils/theme"; import { loadTokenLogo, type Token } from "~tokens/token"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; -import { Text } from "@arconnect/components"; +import { Text, TooltipV2 } from "@arconnect/components"; import { getArPrice } from "~lib/coingecko"; import { usePrice } from "~lib/redstone"; import arLogoLight from "url:/assets/ar/logo_light.png"; @@ -25,6 +25,7 @@ import { getUserAvatar } from "~lib/avatar"; import { abbreviateNumber } from "~utils/format"; export default function Token({ onClick, ...props }: Props) { + const [totalBalance, setTotalBalance] = useState(""); // display theme const theme = useTheme(); @@ -41,6 +42,7 @@ export default function Token({ onClick, ...props }: Props) { const balance = useMemo(() => { const formattedBalance = formatTokenBalance(fractBalance); + setTotalBalance(formattedBalance); const numBalance = parseFloat(formattedBalance.replace(/,/g, "")); return abbreviateNumber(numBalance); }, [fractBalance]); @@ -87,9 +89,11 @@ export default function Token({ onClick, ...props }: Props) { {props?.ao && ao logo} - - {balance} {props.ticker} - + + + {balance} {props.ticker} + + {fiatBalance} From e22ac568295f30ed334e16665fde52114e67397f Mon Sep 17 00:00:00 2001 From: elehmandevelopment Date: Mon, 15 Apr 2024 16:35:30 -0600 Subject: [PATCH 03/19] ci: is million --- src/components/popup/Token.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/popup/Token.tsx b/src/components/popup/Token.tsx index e9bdb2add..360e6af46 100644 --- a/src/components/popup/Token.tsx +++ b/src/components/popup/Token.tsx @@ -26,6 +26,7 @@ import { abbreviateNumber } from "~utils/format"; export default function Token({ onClick, ...props }: Props) { const [totalBalance, setTotalBalance] = useState(""); + const [isMillion, setIsMillion] = useState(false); // display theme const theme = useTheme(); @@ -44,6 +45,7 @@ export default function Token({ onClick, ...props }: Props) { const formattedBalance = formatTokenBalance(fractBalance); setTotalBalance(formattedBalance); const numBalance = parseFloat(formattedBalance.replace(/,/g, "")); + setIsMillion(numBalance >= 1_000_000); return abbreviateNumber(numBalance); }, [fractBalance]); @@ -89,11 +91,18 @@ export default function Token({ onClick, ...props }: Props) { {props?.ao && ao logo} - + {isMillion ? ( + + + {balance} {props.ticker} + + + ) : ( {balance} {props.ticker} - + )} + {fiatBalance} From 9933765290357d205e493c4ff65edc583ed394e1 Mon Sep 17 00:00:00 2001 From: elehmandevelopment Date: Tue, 16 Apr 2024 11:30:24 -0600 Subject: [PATCH 04/19] fix: tooltip position --- src/components/popup/Token.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/popup/Token.tsx b/src/components/popup/Token.tsx index 360e6af46..18e152b51 100644 --- a/src/components/popup/Token.tsx +++ b/src/components/popup/Token.tsx @@ -92,11 +92,11 @@ export default function Token({ onClick, ...props }: Props) { {isMillion ? ( - - + + {balance} {props.ticker} - + ) : ( {balance} {props.ticker} @@ -130,6 +130,10 @@ const Wrapper = styled.div` } `; +const BalanceTooltip = styled(TooltipV2)` + margin-right: 1rem; +`; + const Image = styled.img` width: 16px; padding: 0 8px; From 7449ae81e8f00f2d4b8bb39cef343b137aeff449 Mon Sep 17 00:00:00 2001 From: elehmandevelopment Date: Tue, 16 Apr 2024 13:34:25 -0600 Subject: [PATCH 05/19] fix: sign allowance if 0 --- src/routes/popup/send/confirm.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/routes/popup/send/confirm.tsx b/src/routes/popup/send/confirm.tsx index 941992a69..8cf21fb8f 100644 --- a/src/routes/popup/send/confirm.tsx +++ b/src/routes/popup/send/confirm.tsx @@ -98,17 +98,21 @@ export default function Confirm({ tokenID, qty }: Props) { useEffect(() => { const fetchData = async () => { - let allowance = await ExtensionStorage.get("signatureAllowance"); - if (!allowance) { + let allowance: string | number = await ExtensionStorage.get( + "signatureAllowance" + ); + if (!allowance && Number(allowance) !== 0) { await ExtensionStorage.set("signatureAllowance", 10); - allowance = await ExtensionStorage.get("signatureAllowance"); + allowance = 10; } setSignAllowance(Number(allowance)); try { const data: TransactionData = await TempTransactionStorage.get("send"); if (data) { - if (Number(data.qty) < Number(allowance)) { + if (Number(allowance) !== 0 && Number(data.qty) < Number(allowance)) { setNeedsSign(false); + } else { + setNeedsSign(true); } const estimatedFiatTotal = Number( ( @@ -138,7 +142,7 @@ export default function Confirm({ tokenID, qty }: Props) { fetchData(); trackPage(PageType.CONFIRM_SEND); - }, []); + }, [signAllowance, needsSign]); const [wallets] = useStorage( { From 815a932721693393f1e4d39bbdbc88072a545ac7 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Tue, 16 Apr 2024 15:36:40 -0700 Subject: [PATCH 06/19] refactor: integrated ao-tokens --- package.json | 1 + .../dashboard/subsettings/AddToken.tsx | 6 +- src/components/popup/Token.tsx | 4 +- src/routes/popup/notifications.tsx | 28 +++-- src/routes/popup/send/confirm.tsx | 6 +- src/routes/popup/send/index.tsx | 8 +- src/tokens/aoTokens/ao.ts | 100 +++--------------- yarn.lock | 5 + 8 files changed, 44 insertions(+), 114 deletions(-) diff --git a/package.json b/package.json index f2d68f51e..81801ae65 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@plasmohq/storage": "^1.7.2", "@segment/analytics-next": "^1.53.2", "@untitled-ui/icons-react": "^0.1.1", + "ao-tokens": "^0.0.3", "ar-gql": "^0.0.6", "arbundles": "^0.9.5", "arweave": "^1.13.0", diff --git a/src/components/dashboard/subsettings/AddToken.tsx b/src/components/dashboard/subsettings/AddToken.tsx index 8f386dac9..49cc07fc7 100644 --- a/src/components/dashboard/subsettings/AddToken.tsx +++ b/src/components/dashboard/subsettings/AddToken.tsx @@ -10,7 +10,7 @@ import { import browser from "webextension-polyfill"; import { useEffect, useState } from "react"; import { useAo, type TokenInfo, useAoTokens } from "~tokens/aoTokens/ao"; -import { getTokenInfo } from "~tokens/aoTokens/router"; +import { Token } from "ao-tokens"; import styled from "styled-components"; import { isAddress } from "~utils/assertions"; import { addToken, getAoTokens, getDreForToken, useTokens } from "~tokens"; @@ -83,7 +83,9 @@ export default function AddToken() { //TODO double check isAddress(targetInput.state); if (type === "ao") { - const tokenInfo = await getTokenInfo(targetInput.state, ao); + const token = (await Token(targetInput.state)).info; + const denomination = Number(token.Denomination.toString()); + const tokenInfo: TokenInfo = { ...token, Denomination: denomination }; setToken(tokenInfo); setLoading(false); } else { diff --git a/src/components/popup/Token.tsx b/src/components/popup/Token.tsx index 18e152b51..740bc041b 100644 --- a/src/components/popup/Token.tsx +++ b/src/components/popup/Token.tsx @@ -94,12 +94,12 @@ export default function Token({ onClick, ...props }: Props) { {isMillion ? ( - {balance} {props.ticker} + {props.ao ? props.balance : balance} {props.ticker} ) : ( - {balance} {props.ticker} + {props.ao ? props.balance : balance} {props.ticker} )} diff --git a/src/routes/popup/notifications.tsx b/src/routes/popup/notifications.tsx index 0951ca6d7..99bd77c17 100644 --- a/src/routes/popup/notifications.tsx +++ b/src/routes/popup/notifications.tsx @@ -179,21 +179,19 @@ export default function Notifications() { notifications.map((notification, index) => ( -
- {notification.transactionType === "Message" ? ( -
-
Message
- ao logo -
- ) : ( - "Transaction" - )} +
+
+ {notification.transactionType === "Message" + ? "Message" + : "Transaction"} +
+ {!!notification.isAo && ao logo}
{formatDate(notification.node.block.timestamp)}
diff --git a/src/routes/popup/send/confirm.tsx b/src/routes/popup/send/confirm.tsx index 941992a69..a51035978 100644 --- a/src/routes/popup/send/confirm.tsx +++ b/src/routes/popup/send/confirm.tsx @@ -47,6 +47,7 @@ import { UR } from "@ngraveio/bc-ur"; import { decodeSignature, transactionToUR } from "~wallets/hardware/keystone"; import { useScanner } from "@arconnect/keystone-sdk"; import Progress from "~components/Progress"; +import { Token as TokenInstance } from "ao-tokens"; interface Props { tokenID: string; @@ -314,11 +315,14 @@ export default function Confirm({ tokenID, qty }: Props) { // 2/21/24: Checking first if it's an ao transfer and will handle in this block if (isAo) { try { + const tokenInstance = await TokenInstance(tokenID); + const tokenAmount = + tokenInstance.Quantity.fromString(amount).raw.toString(); const res = await sendAoTransfer( ao, tokenID, recipient.address, - fractionedToBalance(Number(amount), token).toString() + tokenAmount ); if (res) { setToast({ diff --git a/src/routes/popup/send/index.tsx b/src/routes/popup/send/index.tsx index 9bf997a28..acb585353 100644 --- a/src/routes/popup/send/index.tsx +++ b/src/routes/popup/send/index.tsx @@ -236,13 +236,7 @@ export default function Send({ id }: Props) { }) ); } else { - setBalance( - balanceToFractioned(token.balance, { - id: tokenID, - decimals: token.decimals, - divisibility: token.divisibility - }) - ); + setBalance(token.balance); } })(); }, [token, activeAddress, arBalance, id]); diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index 9148d5cc0..08d88afea 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -4,7 +4,8 @@ import { connect } from "@permaweb/aoconnect"; import { type Tag } from "arweave/web/lib/transaction"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; -import { getTokenInfo, useTokenIDs } from "./router"; +import { Token } from "ao-tokens"; +import { useTokenIDs } from "./router"; import { ArweaveSigner, createData } from "arbundles"; import { getActiveKeyfile } from "~wallets"; import { isLocalWallet } from "~utils/assertions"; @@ -41,49 +42,6 @@ export function useAo() { return ao; } -/** - * Token balance hook (integer balance) - */ -export function useBalance(id: string): [number | undefined, boolean] { - // balance - const [balance, setBalance] = useState(0); - - // active address - const [activeAddress] = useStorage({ - key: "active_address", - instance: ExtensionStorage - }); - - // loading - const [loading, setLoading] = useState(true); - - // ao instance - const ao = useAo(); - - useEffect(() => { - (async () => { - if (!activeAddress || !id || id === "") return; - setLoading(true); - - const retry = async (tries = 0) => { - try { - const bal = await getBalance(id, activeAddress, ao); - - setBalance(bal); - } catch { - if (tries >= 5) return; - await retry(tries + 1); - } - }; - await retry(); - - setLoading(false); - })(); - }, [activeAddress, ao]); - - return [balance, loading]; -} - export function useAoTokens(): [TokenInfoWithBalance[], boolean] { const [tokens, setTokens] = useState([]); @@ -157,10 +115,17 @@ export function useAoTokens(): [TokenInfoWithBalance[], boolean] { try { setBalances( await Promise.all( - ids.map(async (id) => ({ - id, - balance: await getBalance(id, activeAddress, ao) - })) + ids.map(async (id) => { + const aoToken = await Token(id); + const balance = Number( + (await aoToken.getBalance(activeAddress)).toLocaleString() + ); + + return { + id, + balance + }; + }) ) ); } catch {} @@ -228,45 +193,6 @@ export const sendAoTransfer = async ( } }; -/** - * Get balance for address - * @param id ID of the token - * @param address Target address - */ -export async function getBalance( - id: string, - address: string, - ao: AoInstance -): Promise { - const timeoutPromise = new Promise((_, reject) => - setTimeout(() => reject(new Error("Timeout after 3 seconds")), 3000) - ); - - const fetchBalance = async () => { - const res = await ao.dryrun({ - Id: "0000000000000000000000000000000000000000001", - Owner: address, - process: id, - tags: [{ name: "Action", value: "Balance" }] - }); - - for (const msg of res.Messages as Message[]) { - const balance = getTagValue("Balance", msg.Tags); - - if (balance) return parseInt(balance); - } - - return 0; - }; - - try { - return await Promise.race([fetchBalance(), timeoutPromise]); - } catch (error) { - console.error(error); - return 0; - } -} - export interface TokenInfo { Name?: string; Ticker?: string; diff --git a/yarn.lock b/yarn.lock index 1c2b10527..ae4b5aac6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4401,6 +4401,11 @@ anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +ao-tokens@^0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/ao-tokens/-/ao-tokens-0.0.3.tgz#55f3e74a1931d842f59d9c6f947eab302434c0b4" + integrity sha512-M8Wzq9VIRYNVKhgcu+UaiKDe/jftmoPbZXGLZtoBKEv4XKdbBBcSapHwYk7Lx2k4Kyz2zTacfDv1SvGGO+PFyQ== + "aproba@^1.0.3 || ^2.0.0", aproba@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" From 63cdd189a72ee450faa165129613043e65861c09 Mon Sep 17 00:00:00 2001 From: elehmandevelopment Date: Wed, 17 Apr 2024 11:26:48 -0600 Subject: [PATCH 07/19] feat: support sending to ANS users --- src/components/Recipient.tsx | 8 ++++++++ src/lib/ans.ts | 11 +++++++++++ src/routes/popup/send/index.tsx | 1 - src/routes/popup/send/recipient.tsx | 3 ++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/components/Recipient.tsx b/src/components/Recipient.tsx index 5ffcba51c..c9f03264b 100644 --- a/src/components/Recipient.tsx +++ b/src/components/Recipient.tsx @@ -15,6 +15,7 @@ import { searchArNSName } from "~lib/arns"; import { useToasts } from "@arconnect/components"; import { formatAddress, isAddressFormat } from "~utils/format"; import { ExtensionStorage } from "~utils/storage"; +import { getAnsProfileByLabel, isANS } from "~lib/ans"; export type Contact = { name: string; @@ -80,6 +81,13 @@ export default function Recipient({ onClick, onClose }: RecipientProps) { return; } else { let search = targetInput.state; + const ANS = isANS(search); + if (ANS) { + const result = await getAnsProfileByLabel(search.slice(0, -3)); + onClick({ address: result.user }); + onClose(); + return; + } if (targetInput.state.startsWith("ar://")) search = targetInput.state.substring(5); const result = await searchArNSName(search); diff --git a/src/lib/ans.ts b/src/lib/ans.ts index 6ce806579..0d148eb50 100644 --- a/src/lib/ans.ts +++ b/src/lib/ans.ts @@ -46,6 +46,17 @@ export async function getAnsProfileByLabel(label: string): Promise { } } +/** + * Checks if search has .ar appended to the end + * If so, it is an ANS address and can call getAnsProfileByLabel() + * @param label string to fetch profile for + */ + +export const isANS = (label: string): boolean => { + const lastThreeLetters = label.slice(-3); + return lastThreeLetters === ".ar"; +}; + /** * React hook for a simple ANS profile * diff --git a/src/routes/popup/send/index.tsx b/src/routes/popup/send/index.tsx index 9bf997a28..3df230fe8 100644 --- a/src/routes/popup/send/index.tsx +++ b/src/routes/popup/send/index.tsx @@ -2,7 +2,6 @@ import { PageType, trackPage } from "~utils/analytics"; import { useState, useEffect, useMemo } from "react"; import styled, { css } from "styled-components"; import { - Button, ButtonV2, InputV2, Section, diff --git a/src/routes/popup/send/recipient.tsx b/src/routes/popup/send/recipient.tsx index 93f82bb9a..1cda8ad56 100644 --- a/src/routes/popup/send/recipient.tsx +++ b/src/routes/popup/send/recipient.tsx @@ -6,6 +6,7 @@ import { } from "~utils/storage"; import { Input, + InputV2, Section, Spacer, Text, @@ -168,7 +169,7 @@ export default function Recipient({ tokenID, qty, message }: Props) {
- Date: Thu, 18 Apr 2024 08:34:23 -0600 Subject: [PATCH 08/19] ci: ans support --- assets/_locales/en/messages.json | 18 ++++++++++++++++-- src/components/Recipient.tsx | 14 +++++++++++++- src/routes/popup/send/recipient.tsx | 1 - 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 3782aa83e..7dfe7da0a 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1312,7 +1312,7 @@ "description": "Label for the address input" }, "transaction_send_address_input_placeholder": { - "message": "Address or ArNS name...", + "message": "Address, ANS or ArNS...", "description": "Placeholder for the address input" }, "transaction_send_scan_address": { @@ -1656,10 +1656,14 @@ "message": "Your Contacts", "desription": "Your Contacts title in Send Modal" }, - "check_address" : { + "check_address": { "message": "Please check the recipient address", "description": "Error message for invalid address/ArNS" }, + "incorrect_address": { + "message": "Incorrect recipient address", + "description": "Error message for invalid ANS" + }, "arns_added": { "message": " ar://$ADDRESS$ added", "description": "ArNS recipient added", @@ -1670,6 +1674,16 @@ } } }, + "ans_added": { + "message": "$ADDRESS$ added", + "description": "ANS recipient added", + "placeholders": { + "address": { + "content": "$1", + "example": "Address" + } + } + }, "setting_ao_support": { "message": "ao support", "description": "ao support settings title" diff --git a/src/components/Recipient.tsx b/src/components/Recipient.tsx index c9f03264b..95e7ec988 100644 --- a/src/components/Recipient.tsx +++ b/src/components/Recipient.tsx @@ -84,8 +84,20 @@ export default function Recipient({ onClick, onClose }: RecipientProps) { const ANS = isANS(search); if (ANS) { const result = await getAnsProfileByLabel(search.slice(0, -3)); + if (!result) { + setToast({ + type: "error", + content: browser.i18n.getMessage("incorrect_address"), + duration: 2400 + }); + } onClick({ address: result.user }); onClose(); + setToast({ + type: "success", + content: browser.i18n.getMessage("ans_added", [search]), + duration: 2400 + }); return; } if (targetInput.state.startsWith("ar://")) @@ -154,7 +166,7 @@ export default function Recipient({ onClick, onClose }: RecipientProps) { }} /> { submit(); }} diff --git a/src/routes/popup/send/recipient.tsx b/src/routes/popup/send/recipient.tsx index 1cda8ad56..f06ee9d9d 100644 --- a/src/routes/popup/send/recipient.tsx +++ b/src/routes/popup/send/recipient.tsx @@ -5,7 +5,6 @@ import { TempTransactionStorage } from "~utils/storage"; import { - Input, InputV2, Section, Spacer, From d6c8d7feefa20deedcf72d8947a1dbd180c07d56 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 17 Apr 2024 17:03:02 -0700 Subject: [PATCH 09/19] feat: started popup for sign data item involving transfers --- assets/_locales/en/messages.json | 14 ++ src/api/modules/connect/auth.ts | 3 +- .../sign_data_item.background.ts | 22 ++- src/routes/auth/signDataItem.tsx | 175 ++++++++++++++++++ src/tabs/auth.tsx | 2 + 5 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 src/routes/auth/signDataItem.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 3782aa83e..b20725603 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1177,10 +1177,24 @@ "message": "Click to view your last signed transaction", "description": "View last tx prompt content" }, + "sign_item": { + "message": "Sign Item", + "description": "Sign message popup title" + }, "titles_signature": { "message": "Sign message", "description": "Sign message popup title" }, + "sign_data_description": { + "message": "$APPNAME$ wants to sign a transaction. Review the details below.", + "description": "Desription for signing an item containing a transfer", + "placeholders": { + "appname": { + "content": "$1", + "example": "permafacts.arweave.dev" + } + } + }, "signature_description": { "message": "$APPNAME$ wants to sign a message. Review the message below.", "description": "App signature request for data that cannot be decoded to string", diff --git a/src/api/modules/connect/auth.ts b/src/api/modules/connect/auth.ts index 5f2787c3b..3e7159de2 100644 --- a/src/api/modules/connect/auth.ts +++ b/src/api/modules/connect/auth.ts @@ -11,7 +11,8 @@ export type AuthType = | "token" | "sign" | "signMessage" - | "signature"; + | "signature" + | "signDataItem"; export interface AuthData { // type of auth to request from the user diff --git a/src/api/modules/sign_data_item/sign_data_item.background.ts b/src/api/modules/sign_data_item/sign_data_item.background.ts index 5fa9a3aa5..682833e81 100644 --- a/src/api/modules/sign_data_item/sign_data_item.background.ts +++ b/src/api/modules/sign_data_item/sign_data_item.background.ts @@ -13,11 +13,9 @@ import { getActiveKeyfile } from "~wallets"; import browser from "webextension-polyfill"; import { signAuth } from "../sign/sign_auth"; import Arweave from "arweave"; +import authenticate from "../connect/auth"; -const background: ModuleFunction = async ( - appData, - dataItem: unknown -) => { +const background: ModuleFunction = async (appData, dataItem: unknown) => { // validate try { isRawDataItem(dataItem); @@ -25,6 +23,22 @@ const background: ModuleFunction = async ( throw new Error(err); } + if ( + dataItem.tags.some( + (tag) => tag.name === "Action" && tag.value === "Transfer" + ) + ) { + try { + await authenticate({ + type: "signDataItem", + data: dataItem, + appData + }); + } catch { + throw new Error("User rejected the sign data item request"); + } + } + // grab the user's keyfile const decryptedWallet = await getActiveKeyfile().catch((e) => { isNotCancelError(e); diff --git a/src/routes/auth/signDataItem.tsx b/src/routes/auth/signDataItem.tsx new file mode 100644 index 000000000..ff0824686 --- /dev/null +++ b/src/routes/auth/signDataItem.tsx @@ -0,0 +1,175 @@ +import { replyToAuthRequest, useAuthParams, useAuthUtils } from "~utils/auth"; +import { + ButtonV2, + InputV2, + ListItem, + Section, + Spacer, + Text, + useInput +} from "@arconnect/components"; +import Wrapper from "~components/auth/Wrapper"; +import browser from "webextension-polyfill"; +import { useEffect, useMemo, useState } from "react"; +import styled from "styled-components"; +import HeadV2 from "~components/popup/HeadV2"; +import { svgie } from "~utils/svgies"; +import { formatAddress } from "~utils/format"; +import { useStorage } from "@plasmohq/storage/hook"; +import { ExtensionStorage } from "~utils/storage"; + +interface Tag { + name: string; + value: string; +} + +interface DataStructure { + data: number[]; + tags: Tag[]; +} + +export default function SignDataItem() { + // connect params + const params = useAuthParams<{ + appData: { appURL: string }; + data: DataStructure; + }>(); + + const [password, setPassword] = useState(false); + + const recipient = + params?.data?.tags.find((tag) => tag.name === "Recipient")?.value || "NA"; + const quantity = + params?.data?.tags.find((tag) => tag.name === "Quantity")?.value || "NA"; + + const [signatureAllowance] = useStorage( + { + key: "signatureAllowance", + instance: ExtensionStorage + }, + 10 + ); + + // get auth utils + const { closeWindow, cancel } = useAuthUtils("signDataItem", params?.authID); + + const passwordInput = useInput(); + + const svgieAvatar = useMemo( + () => svgie(recipient, { asDataURI: true }), + [recipient] + ); + + useEffect(() => { + if (signatureAllowance < Number(quantity)) setPassword(true); + }, [signatureAllowance, quantity]); + + // listen for enter to reset + useEffect(() => { + const listener = async (e: KeyboardEvent) => { + if (e.key !== "Enter") return; + await sign(); + }; + + window.addEventListener("keydown", listener); + + return () => window.removeEventListener("keydown", listener); + }, [params?.authID]); + + // sign message + async function sign() { + // send response + await replyToAuthRequest("signDataItem", params?.authID); + + // close the window + closeWindow(); + } + + return ( + +
+ + + + {browser.i18n.getMessage( + "sign_data_description", + params?.appData.appURL + )} + +
+ + browser.tabs.create({ + url: `https://viewblock.io/arweave/address/${recipient}` + }) + } + /> + {/* TODO: will we have access to process ID? if so would be great to get the token image and redirect to ao scanner */} + +
+
+ +
+
+ + {password && ( + <> + + + + + + )} + + {browser.i18n.getMessage("signature_authorize")} + + + + {browser.i18n.getMessage("cancel")} + +
+
+ ); +} + +const Description = styled(Section)` + display: flex; + flex-direction: column; + gap: 18px; +`; + +const PasswordWrapper = styled.div` + display: flex; + padding-top: 16px; + flex-direction: column; + + p { + text-transform: capitalize; + } +`; diff --git a/src/tabs/auth.tsx b/src/tabs/auth.tsx index 04d054dd9..9ed95a6de 100644 --- a/src/tabs/auth.tsx +++ b/src/tabs/auth.tsx @@ -12,6 +12,7 @@ import Allowance from "~routes/auth/allowance"; import Signature from "~routes/auth/signature"; import Connect from "~routes/auth/connect"; import Unlock from "~routes/auth/unlock"; +import SignDataItem from "~routes/auth/signDataItem"; import Token from "~routes/auth/token"; import Sign from "~routes/auth/sign"; import SignMessage from "~routes/auth/signMessage"; @@ -36,6 +37,7 @@ export default function Auth() { + From f2053ce981c16181ab6f1e1485b3c6aa2a840f47 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 17 Apr 2024 18:42:26 -0700 Subject: [PATCH 10/19] fix: added password for sign data item --- src/routes/auth/signDataItem.tsx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/routes/auth/signDataItem.tsx b/src/routes/auth/signDataItem.tsx index ff0824686..7598c9c44 100644 --- a/src/routes/auth/signDataItem.tsx +++ b/src/routes/auth/signDataItem.tsx @@ -6,7 +6,8 @@ import { Section, Spacer, Text, - useInput + useInput, + useToasts } from "@arconnect/components"; import Wrapper from "~components/auth/Wrapper"; import browser from "webextension-polyfill"; @@ -17,6 +18,7 @@ import { svgie } from "~utils/svgies"; import { formatAddress } from "~utils/format"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; +import { checkPassword } from "~wallets/auth"; interface Tag { name: string; @@ -36,6 +38,7 @@ export default function SignDataItem() { }>(); const [password, setPassword] = useState(false); + const { setToast } = useToasts(); const recipient = params?.data?.tags.find((tag) => tag.name === "Recipient")?.value || "NA"; @@ -78,6 +81,18 @@ export default function SignDataItem() { // sign message async function sign() { + if (password) { + const checkPw = await checkPassword(passwordInput.state); + if (!checkPw) { + setToast({ + type: "error", + content: browser.i18n.getMessage("invalidPassword"), + duration: 2400 + }); + return; + } + } + // send response await replyToAuthRequest("signDataItem", params?.authID); @@ -144,7 +159,6 @@ export default function SignDataItem() { {browser.i18n.getMessage("signature_authorize")} From 69eb2d5e3a59144599bd23075d91e4e3da23f08f Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 18 Apr 2024 15:12:03 -0700 Subject: [PATCH 11/19] fix: added ticker, denom, and logo --- src/components/popup/Token.tsx | 4 -- src/routes/auth/signDataItem.tsx | 68 ++++++++++++++++++++++++++------ 2 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/components/popup/Token.tsx b/src/components/popup/Token.tsx index 740bc041b..f958e1461 100644 --- a/src/components/popup/Token.tsx +++ b/src/components/popup/Token.tsx @@ -77,10 +77,6 @@ export default function Token({ onClick, ...props }: Props) { })(); }, [props, theme, logo]); - useEffect(() => { - console.log("balance:", balance); - }, [balance]); - return ( diff --git a/src/routes/auth/signDataItem.tsx b/src/routes/auth/signDataItem.tsx index 7598c9c44..4ebce13c8 100644 --- a/src/routes/auth/signDataItem.tsx +++ b/src/routes/auth/signDataItem.tsx @@ -3,6 +3,7 @@ import { ButtonV2, InputV2, ListItem, + Loading, Section, Spacer, Text, @@ -19,6 +20,8 @@ import { formatAddress } from "~utils/format"; import { useStorage } from "@plasmohq/storage/hook"; import { ExtensionStorage } from "~utils/storage"; import { checkPassword } from "~wallets/auth"; +import { Quantity, Token } from "ao-tokens"; +import { getUserAvatar } from "~lib/avatar"; interface Tag { name: string; @@ -27,6 +30,7 @@ interface Tag { interface DataStructure { data: number[]; + process?: string; tags: Tag[]; } @@ -38,12 +42,17 @@ export default function SignDataItem() { }>(); const [password, setPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [tokenName, setTokenName] = useState(""); + const [logo, setLogo] = useState(""); + const [amount, setAmount] = useState(null); const { setToast } = useToasts(); const recipient = params?.data?.tags.find((tag) => tag.name === "Recipient")?.value || "NA"; const quantity = params?.data?.tags.find((tag) => tag.name === "Quantity")?.value || "NA"; + const process = params?.data?.process; const [signatureAllowance] = useStorage( { @@ -64,8 +73,38 @@ export default function SignDataItem() { ); useEffect(() => { - if (signatureAllowance < Number(quantity)) setPassword(true); - }, [signatureAllowance, quantity]); + if (signatureAllowance < amount?.toNumber()) { + setPassword(true); + } else { + setPassword(false); + } + }, [signatureAllowance, amount]); + + // get ao token info + useEffect(() => { + const fetchTokenInfo = async () => { + try { + if (params?.data) { + setLoading(true); + const token = await Token(params.data.process); + const logo = await getUserAvatar(token?.info?.Logo || ""); + + const tokenAmount = new Quantity( + BigInt(quantity), + token.info.Denomination + ); + setTokenName(token.info.Name); + setLogo(logo); + setAmount(tokenAmount); + setLoading(false); + } + } catch (err) { + console.log("err", err); + setLoading(false); + } + }; + fetchTokenInfo(); + }, [params]); // listen for enter to reset useEffect(() => { @@ -118,7 +157,6 @@ export default function SignDataItem() {
- {/* TODO: will we have access to process ID? if so would be great to get the token image and redirect to ao scanner */} - + {loading ? ( + + ) : ( + + browser.tabs.create({ + url: `https://ao_marton.g8way.io/#/process/${process}` + }) + } + /> + )}
@@ -159,7 +205,7 @@ export default function SignDataItem() { {browser.i18n.getMessage("signature_authorize")} From 4bf03b4a57b6f453b0de7bc5b7f6e4c95673de6d Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 18 Apr 2024 15:13:49 -0700 Subject: [PATCH 12/19] fix: reverted type change --- src/api/modules/sign_data_item/sign_data_item.background.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/api/modules/sign_data_item/sign_data_item.background.ts b/src/api/modules/sign_data_item/sign_data_item.background.ts index 682833e81..567e6cf5d 100644 --- a/src/api/modules/sign_data_item/sign_data_item.background.ts +++ b/src/api/modules/sign_data_item/sign_data_item.background.ts @@ -15,7 +15,10 @@ import { signAuth } from "../sign/sign_auth"; import Arweave from "arweave"; import authenticate from "../connect/auth"; -const background: ModuleFunction = async (appData, dataItem: unknown) => { +const background: ModuleFunction = async ( + appData, + dataItem: unknown +) => { // validate try { isRawDataItem(dataItem); From e4b1a6fd361085f1e767862b8565e84edb2ad95e Mon Sep 17 00:00:00 2001 From: elehmandevelopment Date: Thu, 18 Apr 2024 16:22:29 -0600 Subject: [PATCH 13/19] fix: use effect --- src/routes/popup/send/confirm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/popup/send/confirm.tsx b/src/routes/popup/send/confirm.tsx index 8cf21fb8f..64ec24381 100644 --- a/src/routes/popup/send/confirm.tsx +++ b/src/routes/popup/send/confirm.tsx @@ -142,7 +142,7 @@ export default function Confirm({ tokenID, qty }: Props) { fetchData(); trackPage(PageType.CONFIRM_SEND); - }, [signAllowance, needsSign]); + }, []); const [wallets] = useStorage( { From 434eb2188c420e5819355fea67385513a1139af4 Mon Sep 17 00:00:00 2001 From: elehmandevelopment Date: Thu, 18 Apr 2024 16:35:42 -0600 Subject: [PATCH 14/19] fix: sign allowance 0 --- src/routes/popup/send/confirm.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/routes/popup/send/confirm.tsx b/src/routes/popup/send/confirm.tsx index 64ec24381..842b519b2 100644 --- a/src/routes/popup/send/confirm.tsx +++ b/src/routes/popup/send/confirm.tsx @@ -19,7 +19,6 @@ import { type RawStoredTransfer } from "~utils/storage"; import { useEffect, useMemo, useState } from "react"; -import { useTokens } from "~tokens"; import { findGateway } from "~gateways/wayfinder"; import Arweave from "arweave"; import { useHistory } from "~utils/hash_router"; @@ -80,7 +79,6 @@ export default function Confirm({ tokenID, qty }: Props) { undefined ); const contact = useContact(recipient?.address); - // TODO: Remove const [signAllowance, setSignAllowance] = useState(10); const [needsSign, setNeedsSign] = useState(true); const { setToast } = useToasts(); @@ -98,6 +96,9 @@ export default function Confirm({ tokenID, qty }: Props) { useEffect(() => { const fetchData = async () => { + let fetchedSignAllowance = 0; + let fetchedNeedsSign = false; + let allowance: string | number = await ExtensionStorage.get( "signatureAllowance" ); @@ -105,14 +106,15 @@ export default function Confirm({ tokenID, qty }: Props) { await ExtensionStorage.set("signatureAllowance", 10); allowance = 10; } - setSignAllowance(Number(allowance)); + fetchedSignAllowance = Number(allowance); + try { const data: TransactionData = await TempTransactionStorage.get("send"); if (data) { if (Number(allowance) !== 0 && Number(data.qty) < Number(allowance)) { - setNeedsSign(false); + fetchedNeedsSign = false; } else { - setNeedsSign(true); + fetchedNeedsSign = true; } const estimatedFiatTotal = Number( ( @@ -135,6 +137,9 @@ export default function Confirm({ tokenID, qty }: Props) { } else { push("/send/transfer"); } + + setSignAllowance(fetchedSignAllowance); + setNeedsSign(fetchedNeedsSign); } catch (error) { console.error("Error fetching data:", error); } From a6b74b12b4b66fa07715e482ee7f6a27afbcdfd9 Mon Sep 17 00:00:00 2001 From: Ethan <117924723+elehmandevelopment@users.noreply.github.com> Date: Thu, 18 Apr 2024 21:30:30 -0600 Subject: [PATCH 15/19] chore: update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9c888f3a6..1fc2763c6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "arconnect", "displayName": "ArConnect", - "version": "1.9.0", + "version": "1.10.0", "description": "__MSG_extensionDescription__", "author": "th8ta", "packageManager": "yarn@1.22.18", From 9475a81b147d4d03372650d80fd45acd50f862cb Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Tue, 23 Apr 2024 15:20:48 -0700 Subject: [PATCH 16/19] fix: updated process to direct to target in signdataitem --- .../modules/sign_data_item/sign_data_item.background.ts | 3 +++ src/routes/auth/signDataItem.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/api/modules/sign_data_item/sign_data_item.background.ts b/src/api/modules/sign_data_item/sign_data_item.background.ts index 567e6cf5d..69f6778c7 100644 --- a/src/api/modules/sign_data_item/sign_data_item.background.ts +++ b/src/api/modules/sign_data_item/sign_data_item.background.ts @@ -29,6 +29,9 @@ const background: ModuleFunction = async ( if ( dataItem.tags.some( (tag) => tag.name === "Action" && tag.value === "Transfer" + ) && + dataItem.tags.some( + (tag) => tag.name === "Data-Protocol" && tag.value === "ao" ) ) { try { diff --git a/src/routes/auth/signDataItem.tsx b/src/routes/auth/signDataItem.tsx index 4ebce13c8..ac2710077 100644 --- a/src/routes/auth/signDataItem.tsx +++ b/src/routes/auth/signDataItem.tsx @@ -30,7 +30,7 @@ interface Tag { interface DataStructure { data: number[]; - process?: string; + target?: string; tags: Tag[]; } @@ -52,7 +52,7 @@ export default function SignDataItem() { params?.data?.tags.find((tag) => tag.name === "Recipient")?.value || "NA"; const quantity = params?.data?.tags.find((tag) => tag.name === "Quantity")?.value || "NA"; - const process = params?.data?.process; + const process = params?.data?.target; const [signatureAllowance] = useStorage( { @@ -84,9 +84,9 @@ export default function SignDataItem() { useEffect(() => { const fetchTokenInfo = async () => { try { - if (params?.data) { + if (process) { setLoading(true); - const token = await Token(params.data.process); + const token = await Token(params.data.target); const logo = await getUserAvatar(token?.info?.Logo || ""); const tokenAmount = new Quantity( From 6f6eecd0afb6dd141c7ba8dbfc0077f6410af21c Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Tue, 23 Apr 2024 15:39:01 -0700 Subject: [PATCH 17/19] chore: version bump for beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1fc2763c6..3b5b4abce 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "arconnect", "displayName": "ArConnect", - "version": "1.10.0", + "version": "1.10.1", "description": "__MSG_extensionDescription__", "author": "th8ta", "packageManager": "yarn@1.22.18", From 694f6aa30fe8bd7dd9b7031632c56d772856fff0 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 24 Apr 2024 11:24:14 -0700 Subject: [PATCH 18/19] fix: removed to locale string --- src/tokens/aoTokens/ao.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/tokens/aoTokens/ao.ts b/src/tokens/aoTokens/ao.ts index 08d88afea..a557f12a6 100644 --- a/src/tokens/aoTokens/ao.ts +++ b/src/tokens/aoTokens/ao.ts @@ -117,9 +117,7 @@ export function useAoTokens(): [TokenInfoWithBalance[], boolean] { await Promise.all( ids.map(async (id) => { const aoToken = await Token(id); - const balance = Number( - (await aoToken.getBalance(activeAddress)).toLocaleString() - ); + const balance = Number(await aoToken.getBalance(activeAddress)); return { id, From 533548f4f53e4b0106006f6cb06fa5131c1852aa Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Wed, 24 Apr 2024 11:27:44 -0700 Subject: [PATCH 19/19] chore: update version for beta --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b5b4abce..fba3345eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "arconnect", "displayName": "ArConnect", - "version": "1.10.1", + "version": "1.10.2", "description": "__MSG_extensionDescription__", "author": "th8ta", "packageManager": "yarn@1.22.18",