From f278cc4f483496656357738039f308925024ffdc Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Mon, 30 Sep 2024 14:25:16 -0700 Subject: [PATCH 1/7] feat: init batch sign data items --- assets/_locales/en/messages.json | 14 + assets/_locales/zh_CN/messages.json | 14 + src/api/background.ts | 5 +- src/api/foreground.ts | 11 +- .../batch_sign_data_item.background.ts | 78 ++++++ .../batch_sign_data_item.foreground.ts | 40 +++ src/api/modules/batch_sign_data_item/index.ts | 11 + src/api/modules/connect/auth.ts | 3 +- src/components/signDataItem/index.tsx | 254 ++++++++++++++++++ src/routes/auth/batchSignDataItem.tsx | 148 ++++++++++ src/tabs/auth.tsx | 2 + 11 files changed, 577 insertions(+), 3 deletions(-) create mode 100644 src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts create mode 100644 src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts create mode 100644 src/api/modules/batch_sign_data_item/index.ts create mode 100644 src/components/signDataItem/index.tsx create mode 100644 src/routes/auth/batchSignDataItem.tsx diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json index 452fae32b..764d5e980 100644 --- a/assets/_locales/en/messages.json +++ b/assets/_locales/en/messages.json @@ -1329,6 +1329,10 @@ "message": "Sign Item", "description": "Sign message popup title" }, + "batch_sign_items": { + "message": "Batch Sign Items", + "description": "Batch sign message popup title" + }, "titles_signature": { "message": "Sign message", "description": "Sign message popup title" @@ -1343,6 +1347,16 @@ } } }, + "batch_sign_data_description": { + "message": "$APPNAME$ wants to sign the following transactions. 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/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json index b2c6e7c3d..b6484314a 100644 --- a/assets/_locales/zh_CN/messages.json +++ b/assets/_locales/zh_CN/messages.json @@ -1317,6 +1317,10 @@ "message": "签署项目", "description": "Sign message popup title" }, + "batch_sign_items": { + "message": "批量签署项目", + "description": "批量签署消息弹出标题" + }, "titles_signature": { "message": "签署消息", "description": "Sign message popup title" @@ -1331,6 +1335,16 @@ } } }, + "batch_sign_data_description": { + "message": "$APPNAME$ 想要签署以下交易。请查看以下详细信息。", + "description": "Description for signing an item containing a transfer", + "placeholders": { + "appname": { + "content": "$1", + "example": "permafacts.arweave.dev" + } + } + }, "signature_description": { "message": "$APPNAME$ 想要签署一条消息。请查看以下消息。", "description": "App signature request for data that cannot be decoded to string", diff --git a/src/api/background.ts b/src/api/background.ts index 9980ce8f2..0be5da8e5 100644 --- a/src/api/background.ts +++ b/src/api/background.ts @@ -39,6 +39,8 @@ import verifyMessageModule from "./modules/verify_message"; import verifyMessage from "./modules/verify_message/verify_message.background"; import signDataItemModule from "./modules/sign_data_item"; import signDataItem from "./modules/sign_data_item/sign_data_item.background"; +import batchSignDataItemModule from "./modules/batch_sign_data_item"; +import batchSignDataItem from "./modules/batch_sign_data_item/batch_sign_data_item.background"; import subscriptionModule from "./modules/subscription"; import subscription from "./modules/subscription/subscription.background"; import userTokensModule from "./modules/user_tokens"; @@ -66,7 +68,8 @@ const modules: BackgroundModule[] = [ { ...verifyMessageModule, function: verifyMessage }, { ...signDataItemModule, function: signDataItem }, { ...subscriptionModule, function: subscription }, - { ...userTokensModule, function: userTokens } + { ...userTokensModule, function: userTokens }, + { ...batchSignDataItemModule, function: batchSignDataItem } ]; export default modules; diff --git a/src/api/foreground.ts b/src/api/foreground.ts index 8b32738a2..cd0ca12b2 100644 --- a/src/api/foreground.ts +++ b/src/api/foreground.ts @@ -55,6 +55,10 @@ import privateHash, { } from "./modules/private_hash/private_hash.foreground"; import verifyMessageModule from "./modules/verify_message"; import verifyMessage from "./modules/verify_message/verify_message.foreground"; +import batchSignDataItemModule from "./modules/batch_sign_data_item"; +import batchSignDataItem, { + finalizer as batchSignDataItemFinalizer +} from "./modules/batch_sign_data_item/batch_sign_data_item.foreground"; import signDataItemModule from "./modules/sign_data_item"; import signDataItem, { finalizer as signDataItemFinalizer @@ -96,7 +100,12 @@ const modules: ForegroundModule[] = [ finalizer: signDataItemFinalizer }, { ...subscriptionModule, function: subscription }, - { ...userTokensModule, function: userTokens } + { ...userTokensModule, function: userTokens }, + { + ...batchSignDataItemModule, + function: batchSignDataItem, + finalizer: batchSignDataItemFinalizer + } ]; export default modules; diff --git a/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts b/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts new file mode 100644 index 000000000..3a968e1ef --- /dev/null +++ b/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts @@ -0,0 +1,78 @@ +import BigNumber from "bignumber.js"; +import type { ModuleFunction } from "~api/module"; +import Application from "~applications/application"; +import { isNotCancelError, isRawDataItem } from "~utils/assertions"; +import authenticate from "../connect/auth"; +import browser from "webextension-polyfill"; +import { getActiveKeyfile } from "~wallets"; +import { freeDecryptedWallet } from "~wallets/encryption"; +import Arweave from "arweave"; +import { ArweaveSigner, createData, DataItem } from "arbundles"; + +interface RawDataItem { + data: number[]; + tags?: { name: string; value: string }[]; +} + +const background: ModuleFunction = async ( + appData, + dataItems: unknown[] +) => { + // validate + if (!Array.isArray(dataItems)) { + throw new Error("Input must be an array of data items"); + } + + for (const dataItem of dataItems) { + isRawDataItem(dataItem); + } + + const app = new Application(appData.appURL); + + // const allowance = await app.getAllowance(); + // const alwaysAsk = allowance.enabled && allowance.limit.eq(BigNumber("0")); + const results: number[][] = []; + + await authenticate({ + type: "batchSignDataItem", + data: dataItems, + appData + }); + + // grab the user's keyfile + const decryptedWallet = await getActiveKeyfile().catch((e) => { + isNotCancelError(e); + + // if there are no wallets added, open the welcome page + browser.tabs.create({ url: browser.runtime.getURL("tabs/welcome.html") }); + + throw new Error("No wallets added"); + }); + + try { + if (decryptedWallet.type !== "local") { + throw new Error("Only local wallets are supported for batch signing"); + } + + const dataSigner = new ArweaveSigner(decryptedWallet.keyfile); + + for (const dataItem of dataItems as RawDataItem[]) { + const { data, ...options } = dataItem; + const binaryData = new Uint8Array(data); + + const dataEntry = createData(binaryData, dataSigner, options); + + await dataEntry.sign(dataSigner); + + results.push(Array.from(dataEntry.getRaw())); + } + } finally { + // Clean up: remove wallet from memory + // @ts-expect-error + freeDecryptedWallet(decryptedWallet.keyfile); + } + + return results; +}; + +export default background; diff --git a/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts b/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts new file mode 100644 index 000000000..38525cb82 --- /dev/null +++ b/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts @@ -0,0 +1,40 @@ +import type { TransformFinalizer } from "~api/foreground"; +import type { ModuleFunction } from "~api/module"; +import type { RawDataItem, SignDataItemParams } from "../sign_data_item/types"; +import { isArrayBuffer } from "~utils/assertions"; + +const foreground: ModuleFunction[]> = async ( + dataItems: SignDataItemParams[] +) => { + if (!Array.isArray(dataItems)) { + throw new Error("Input must be an array of data items"); + } + + const rawDataItems: RawDataItem[] = dataItems.map((dataItem) => { + let rawDataItem: RawDataItem; + + if (typeof dataItem.data !== "string") { + isArrayBuffer(dataItem.data); + + rawDataItem = { + ...dataItem, + data: Array.from(dataItem.data) + }; + } else { + rawDataItem = { + ...dataItem, + data: Array.from(new TextEncoder().encode(dataItem.data)) + }; + } + + return rawDataItem; + }); + + return [rawDataItems]; +}; + +export const finalizer: TransformFinalizer = (result) => { + return result.map((item) => new Uint8Array(item).buffer); +}; + +export default foreground; diff --git a/src/api/modules/batch_sign_data_item/index.ts b/src/api/modules/batch_sign_data_item/index.ts new file mode 100644 index 000000000..473f4ba3c --- /dev/null +++ b/src/api/modules/batch_sign_data_item/index.ts @@ -0,0 +1,11 @@ +import type { PermissionType } from "~applications/permissions"; +import type { ModuleProperties } from "~api/module"; + +const permissions: PermissionType[] = ["SIGN_TRANSACTION"]; + +const batchSignDataItem: ModuleProperties = { + functionName: "batchSignDataItem", + permissions +}; + +export default batchSignDataItem; diff --git a/src/api/modules/connect/auth.ts b/src/api/modules/connect/auth.ts index dd9bbadaf..da779e238 100644 --- a/src/api/modules/connect/auth.ts +++ b/src/api/modules/connect/auth.ts @@ -13,7 +13,8 @@ export type AuthType = | "subscription" | "signKeystone" | "signature" - | "signDataItem"; + | "signDataItem" + | "batchSignDataItem"; export interface AuthData { // type of auth to request from the user diff --git a/src/components/signDataItem/index.tsx b/src/components/signDataItem/index.tsx new file mode 100644 index 000000000..93a627053 --- /dev/null +++ b/src/components/signDataItem/index.tsx @@ -0,0 +1,254 @@ +import { replyToAuthRequest, useAuthParams, useAuthUtils } from "~utils/auth"; +import { + ButtonV2, + InputV2, + Loading, + Section, + Spacer, + Text, + useInput, + useToasts +} from "@arconnect/components"; +import { + FiatAmount, + AmountTitle, + Properties, + TransactionProperty, + PropertyName, + PropertyValue, + TagValue, + useAdjustAmountTitleWidth +} from "~routes/popup/transaction/[id]"; +import Wrapper from "~components/auth/Wrapper"; +import browser from "webextension-polyfill"; +import { useEffect, useMemo, useRef, useState } from "react"; +import styled from "styled-components"; +import HeadV2 from "~components/popup/HeadV2"; +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 prettyBytes from "pretty-bytes"; +import { formatFiatBalance } from "~tokens/currency"; +import useSetting from "~settings/hook"; +import { getPrice } from "~lib/coingecko"; +import type { TokenInfo, TokenInfoWithProcessId } from "~tokens/aoTokens/ao"; +import { ChevronUpIcon, ChevronDownIcon } from "@iconicicons/react"; +import { getUserAvatar } from "~lib/avatar"; +import { LogoWrapper, Logo, WarningIcon } from "~components/popup/Token"; +import arLogoLight from "url:/assets/ar/logo_light.png"; +import arLogoDark from "url:/assets/ar/logo_dark.png"; +import { useTheme } from "~utils/theme"; +import { checkWalletBits, type WalletBitsCheck } from "~utils/analytics"; +import { Degraded, WarningWrapper } from "~routes/popup/send"; + +export default function SignDataItemDetails({ params }) { + const [loading, setLoading] = useState(false); + const [tokenName, setTokenName] = useState(""); + const [logo, setLogo] = useState(""); + const [amount, setAmount] = useState(null); + const [showTags, setShowTags] = useState(false); + + const recipient = + params?.tags?.find((tag) => tag.name === "Recipient")?.value || ""; + const quantity = + params?.tags?.find((tag) => tag.name === "Quantity")?.value || "0"; + const transfer = params?.tags?.some( + (tag) => tag.name === "Action" && tag.value === "Transfer" + ); + + const theme = useTheme(); + const arweaveLogo = useMemo( + () => (theme === "dark" ? arLogoDark : arLogoLight), + [theme] + ); + + useEffect(() => { + const fetchTokenInfo = async () => { + if (!process || !transfer) return; + let tokenInfo: TokenInfo; + try { + setLoading(true); + const token = await Token(params.target); + tokenInfo = { + ...token.info, + Denomination: Number(token.info.Denomination) + }; + } catch (err) { + // fallback + console.log("err", err); + try { + const aoTokens = + (await ExtensionStorage.get( + "ao_tokens" + )) || []; + const aoTokensCache = + (await ExtensionStorage.get( + "ao_tokens_cache" + )) || []; + const aoTokensCombined = [...aoTokens, ...aoTokensCache]; + const token = aoTokensCombined.find( + ({ processId }) => params.target === processId + ); + if (token) { + tokenInfo = token; + } + } catch {} + } finally { + if (tokenInfo) { + if (tokenInfo?.Logo) { + const logo = await getUserAvatar(tokenInfo?.Logo); + setLogo(logo || ""); + } else { + setLogo(arweaveLogo); + } + + const tokenAmount = new Quantity( + BigInt(quantity), + BigInt(tokenInfo.Denomination) + ); + setTokenName(tokenInfo.Name); + setAmount(tokenAmount); + } + setLoading(false); + } + }; + fetchTokenInfo(); + }, [params]); + + // currency setting + const [currency] = useSetting("currency"); + + // token price + const [price, setPrice] = useState(0); + + // transaction price + const fiatPrice = useMemo(() => +(amount || 0) * price, [amount]); + + const process = params?.target; + + const formattedAmount = useMemo( + () => (amount || 0).toLocaleString(), + [amount] + ); + + // active address + const [activeAddress] = useStorage( + { + key: "active_address", + instance: ExtensionStorage + }, + "" + ); + // adjust amount title font sizes + const parentRef = useRef(null); + const childRef = useRef(null); + return ( + <> + {params ? ( +
+
+ {!loading ? ( + logo && ( + + + + ) + ) : ( + + )} +
+ {transfer && ( + <> + {formatFiatBalance(fiatPrice, currency)} + + {formattedAmount} + {tokenName} + + + )} + + + {params?.target && ( + + + {browser.i18n.getMessage("process_id")} + + + {formatAddress(params?.target, 6)} + + + )} + + + {browser.i18n.getMessage("transaction_from")} + + {formatAddress(activeAddress, 6)} + + {recipient && ( + + + {browser.i18n.getMessage("transaction_to")} + + {formatAddress(recipient, 6)} + + )} + + + + {browser.i18n.getMessage("transaction_fee")} + + 0 AR + + + + {browser.i18n.getMessage("transaction_size")} + + {prettyBytes(params?.data.length)} + + + setShowTags(!showTags)} + > + {browser.i18n.getMessage("transaction_tags")} + {showTags ? : } + + {console.log(params)} + + {showTags && + params?.tags?.map((tag, i) => ( + + {tag.name} + {tag.value} + + ))} + +
+ ) : ( + + )} + + ); +} diff --git a/src/routes/auth/batchSignDataItem.tsx b/src/routes/auth/batchSignDataItem.tsx new file mode 100644 index 000000000..19ac59c28 --- /dev/null +++ b/src/routes/auth/batchSignDataItem.tsx @@ -0,0 +1,148 @@ +import { replyToAuthRequest, useAuthParams, useAuthUtils } from "~utils/auth"; +import { ButtonV2, ListItem, Section, Text } from "@arconnect/components"; +import Wrapper from "~components/auth/Wrapper"; +import browser from "webextension-polyfill"; +import { useRef, useState } from "react"; +import styled from "styled-components"; + +import { ResetButton } from "~components/dashboard/Reset"; +import SignDataItemDetails from "~components/signDataItem"; +import HeadV2 from "~components/popup/HeadV2"; + +interface Tag { + name: string; + value: string; +} + +interface DataStructure { + data: number[]; + target?: string; + tags: Tag[]; +} + +export default function BatchSignDataItem() { + // connect params + const params = useAuthParams<{ + appData: { appURL: string }; + data: DataStructure; + }>(); + + const { closeWindow, cancel } = useAuthUtils( + "batchSignDataItem", + params?.authID + ); + + // sign message + + const [transaction, setTransaction] = useState(null); + async function sign() { + // send response + await replyToAuthRequest("signDataItem", params?.authID); + + // close the window + closeWindow(); + } + + return ( + +
+ (transaction ? setTransaction(null) : cancel())} + /> + + + {browser.i18n.getMessage( + "batch_sign_data_description", + params?.appData.appURL + )} + + + + {transaction ? ( + + ) : ( +
+ {Array.isArray(params?.data) && + params.data.map((item, index) => { + return ( + setTransaction(item)} + /> + ); + })} +
+ )} +
+ +
+ {!transaction ? ( + <> + + {browser.i18n.getMessage("signature_authorize")} + + + {browser.i18n.getMessage("cancel")} + + + ) : ( + setTransaction(null)}> + {browser.i18n.getMessage("continue")} + + )} +
+
+ ); +} + +function formatTransactionDescription(tags: Tag[]): string { + console.log("tags", tags); + const actionTag = tags.find((tag) => tag.name === "Action"); + console.log("action", actionTag); + if (actionTag) { + if (actionTag.value === "Transfer") { + const sentTag = tags.find((tag) => tag.name === "Sent"); + const fromProcessTag = tags.find((tag) => tag.name === "From-Process"); + if (sentTag && fromProcessTag) { + return `Sending ${sentTag.value} of ${fromProcessTag.value}`; + } + } else { + try { + const actionData = JSON.parse(actionTag.value); + if (actionData.cmd === "register") { + return `Registering beaver ${actionData.beaverId} with balance ${actionData.balance}`; + } + } catch (e) { + // If JSON parsing fails, we'll fall through to the default return + } + } + } + return "Unknown transaction"; +} + +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 54dd641e5..574ea18d5 100644 --- a/src/tabs/auth.tsx +++ b/src/tabs/auth.tsx @@ -17,6 +17,7 @@ import Token from "~routes/auth/token"; import Sign from "~routes/auth/sign"; import Subscription from "~routes/auth/subscription"; import SignKeystone from "~routes/auth/signKeystone"; +import BatchSignDataItem from "~routes/auth/batchSignDataItem"; export default function Auth() { const theme = useTheme(); @@ -40,6 +41,7 @@ export default function Auth() { + From 9f22323e6e6c4972669cdd51a78a46863d28bb00 Mon Sep 17 00:00:00 2001 From: Pawan Paudel Date: Tue, 1 Oct 2024 22:47:29 +0545 Subject: [PATCH 2/7] fix: Prevent popup freeze by sending keep-alive alarm to background --- src/api/modules/connect/auth.ts | 56 +++++++++++++++++++++++++++++++-- src/background.ts | 7 +++++ src/utils/mutex.ts | 14 +++++++++ 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 src/utils/mutex.ts diff --git a/src/api/modules/connect/auth.ts b/src/api/modules/connect/auth.ts index dd9bbadaf..1efdb4b50 100644 --- a/src/api/modules/connect/auth.ts +++ b/src/api/modules/connect/auth.ts @@ -3,6 +3,11 @@ import { objectToUrlParams } from "./url"; import type { AuthResult } from "shim"; import { nanoid } from "nanoid"; import browser from "webextension-polyfill"; +import { Mutex } from "~utils/mutex"; + +const mutex = new Mutex(); +let keepAliveInterval: number | null = null; +let activePopups = 0; export type AuthType = | "connect" @@ -74,8 +79,11 @@ async function createAuthPopup(data: AuthData) { * Await for a browser message from the popup */ const result = (authID: string, tabId: number) => - new Promise((resolve, reject) => + new Promise(async (resolve, reject) => { + startKeepAlive(); + onMessage("auth_result", ({ sender, data }) => { + stopKeepAlive(); // validate sender by it's tabId if (sender.tabId !== tabId) { return; @@ -93,5 +101,47 @@ const result = (authID: string, tabId: number) => } else { resolve(data); } - }) - ); + }); + }); + +/** + * Function to send periodic keep-alive messages + */ +const startKeepAlive = async () => { + const unlock = await mutex.lock(); + + try { + // Increment the active popups count + activePopups++; + if (activePopups > 0 && keepAliveInterval === null) { + console.log("Started keep-alive messages..."); + keepAliveInterval = setInterval( + () => browser.alarms.create("keep-alive", { when: Date.now() + 1 }), + 20000 + ); + } + } finally { + unlock(); + } +}; + +/** + * Function to stop sending keep-alive messages + */ +const stopKeepAlive = async () => { + const unlock = await mutex.lock(); + + try { + // Decrement the active popups count + activePopups--; + if (activePopups <= 0 && keepAliveInterval !== null) { + // Stop keep-alive messages when no popups are active + browser.alarms.clear("keep-alive"); + clearInterval(keepAliveInterval); + keepAliveInterval = null; + console.log("Stopped keep-alive messages..."); + } + } finally { + unlock(); + } +}; diff --git a/src/background.ts b/src/background.ts index 59dd18d10..40a15215e 100644 --- a/src/background.ts +++ b/src/background.ts @@ -54,6 +54,13 @@ browser.alarms.onAlarm.addListener(keyRemoveAlarmListener); // handle importing ao tokens browser.alarms.onAlarm.addListener(importAoTokens); +// handle keep alive alarm +browser.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === "keep-alive") { + console.log("keep alive alarm"); + } +}); + // handle window close browser.windows.onRemoved.addListener(onWindowClose); diff --git a/src/utils/mutex.ts b/src/utils/mutex.ts new file mode 100644 index 000000000..412e49e5f --- /dev/null +++ b/src/utils/mutex.ts @@ -0,0 +1,14 @@ +export class Mutex { + private mutex = Promise.resolve(); + + lock(): Promise<() => void> { + let unlockNext: () => void; + const willLock = new Promise((resolve) => (unlockNext = resolve)); + // To release the lock + const unlock = () => unlockNext(); + + const currentLock = this.mutex.then(() => unlock); + this.mutex = this.mutex.then(() => willLock); + return currentLock; + } +} From 9225881cd9a624b1379cc2f7847eebadb3e99794 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Tue, 1 Oct 2024 17:44:13 -0700 Subject: [PATCH 3/7] feat: added in ao token details to batch sign --- .../batch_sign_data_item.background.ts | 9 +- src/routes/auth/batchSignDataItem.tsx | 185 +++++++++++++----- 2 files changed, 134 insertions(+), 60 deletions(-) diff --git a/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts b/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts index 3a968e1ef..b39a77cc5 100644 --- a/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts +++ b/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts @@ -27,10 +27,6 @@ const background: ModuleFunction = async ( isRawDataItem(dataItem); } - const app = new Application(appData.appURL); - - // const allowance = await app.getAllowance(); - // const alwaysAsk = allowance.enabled && allowance.limit.eq(BigNumber("0")); const results: number[][] = []; await authenticate({ @@ -51,7 +47,9 @@ const background: ModuleFunction = async ( try { if (decryptedWallet.type !== "local") { - throw new Error("Only local wallets are supported for batch signing"); + throw new Error( + "Only local wallets are currently supported for batch signing" + ); } const dataSigner = new ArweaveSigner(decryptedWallet.keyfile); @@ -67,7 +65,6 @@ const background: ModuleFunction = async ( results.push(Array.from(dataEntry.getRaw())); } } finally { - // Clean up: remove wallet from memory // @ts-expect-error freeDecryptedWallet(decryptedWallet.keyfile); } diff --git a/src/routes/auth/batchSignDataItem.tsx b/src/routes/auth/batchSignDataItem.tsx index 19ac59c28..47f1d56c0 100644 --- a/src/routes/auth/batchSignDataItem.tsx +++ b/src/routes/auth/batchSignDataItem.tsx @@ -1,13 +1,26 @@ import { replyToAuthRequest, useAuthParams, useAuthUtils } from "~utils/auth"; -import { ButtonV2, ListItem, Section, Text } from "@arconnect/components"; +import { + ButtonV2, + InputV2, + ListItem, + Section, + Text, + useInput, + useToasts +} from "@arconnect/components"; import Wrapper from "~components/auth/Wrapper"; import browser from "webextension-polyfill"; -import { useRef, useState } from "react"; +import { useEffect, useState } from "react"; import styled from "styled-components"; import { ResetButton } from "~components/dashboard/Reset"; import SignDataItemDetails from "~components/signDataItem"; import HeadV2 from "~components/popup/HeadV2"; +import { Quantity, Token } from "ao-tokens"; +import { timeoutPromise } from "~tokens/aoTokens/ao"; +import { ExtensionStorage } from "~utils/storage"; +import { useStorage } from "@plasmohq/storage/hook"; +import { checkPassword } from "~wallets/auth"; interface Tag { name: string; @@ -26,23 +39,101 @@ export default function BatchSignDataItem() { appData: { appURL: string }; data: DataStructure; }>(); - + const { setToast } = useToasts(); + const [loading, setLoading] = useState(false); + const [transaction, setTransaction] = useState(null); + const [transactionList, setTransactionList] = useState(null); + const [password, setPassword] = useState(false); const { closeWindow, cancel } = useAuthUtils( "batchSignDataItem", params?.authID ); - - // sign message - - const [transaction, setTransaction] = useState(null); + const passwordInput = useInput(); async function sign() { - // send response - await replyToAuthRequest("signDataItem", params?.authID); - - // close the window + if (password) { + const checkPw = await checkPassword(passwordInput.state); + if (!checkPw) { + setToast({ + type: "error", + content: browser.i18n.getMessage("invalidPassword"), + duration: 2400 + }); + return; + } + } + await replyToAuthRequest("batchSignDataItem", params?.authID); closeWindow(); } + const [signatureAllowance] = useStorage( + { + key: "signatureAllowance", + instance: ExtensionStorage + }, + 10 + ); + + useEffect(() => { + const fetchTransactionList = async () => { + setLoading(true); + try { + if (Array.isArray(params?.data)) { + const listItems = await Promise.all( + params.data.map(async (item, index) => { + let amount = ""; + let name = ""; + const quantity = + item?.tags?.find((tag) => tag.name === "Quantity")?.value || + "0"; + const transfer = item?.tags?.some( + (tag) => tag.name === "Action" && tag.value === "Transfer" + ); + + if (transfer && quantity) { + let tokenInfo: any; + try { + const token = await timeoutPromise(Token(item.target), 6000); + tokenInfo = { + ...token.info, + Denomination: Number(token.info.Denomination) + }; + const tokenAmount = new Quantity( + BigInt(quantity), + BigInt(tokenInfo.Denomination) + ); + amount = tokenAmount.toLocaleString(); + name = tokenInfo.Name; + console.log(signatureAllowance, Number(amount)); + if (signatureAllowance > Number(amount)) { + setPassword(true); + } + } catch (error) { + console.error("Token fetch timed out or failed", error); + amount = quantity; + name = item.target; + } + } + return ( + setTransaction(item)} + /> + ); + }) + ); + setTransactionList(listItems); + } + } finally { + setLoading(false); + } + }; + + fetchTransactionList(); + }, [params]); + return (
@@ -64,17 +155,7 @@ export default function BatchSignDataItem() { ) : (
- {Array.isArray(params?.data) && - params.data.map((item, index) => { - return ( - setTransaction(item)} - /> - ); - })} + {transactionList}
)}
@@ -89,7 +170,28 @@ export default function BatchSignDataItem() { > {!transaction ? ( <> - + {password && ( +
+ { + if (e.key !== "Enter") return; + await sign(); + }} + fullWidth + /> +
+ )} + + {browser.i18n.getMessage("signature_authorize")} @@ -106,27 +208,12 @@ export default function BatchSignDataItem() { ); } -function formatTransactionDescription(tags: Tag[]): string { - console.log("tags", tags); - const actionTag = tags.find((tag) => tag.name === "Action"); - console.log("action", actionTag); - if (actionTag) { - if (actionTag.value === "Transfer") { - const sentTag = tags.find((tag) => tag.name === "Sent"); - const fromProcessTag = tags.find((tag) => tag.name === "From-Process"); - if (sentTag && fromProcessTag) { - return `Sending ${sentTag.value} of ${fromProcessTag.value}`; - } - } else { - try { - const actionData = JSON.parse(actionTag.value); - if (actionData.cmd === "register") { - return `Registering beaver ${actionData.beaverId} with balance ${actionData.balance}`; - } - } catch (e) { - // If JSON parsing fails, we'll fall through to the default return - } - } +function formatTransactionDescription( + amount?: string, + tokenName?: string +): string { + if (amount && tokenName) { + return `Sending ${amount} of ${tokenName}`; } return "Unknown transaction"; } @@ -136,13 +223,3 @@ const Description = styled(Section)` flex-direction: column; gap: 18px; `; - -const PasswordWrapper = styled.div` - display: flex; - padding-top: 16px; - flex-direction: column; - - p { - text-transform: capitalize; - } -`; From 5aae88f8b039548098efbb06478d4d4971e3c1db Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 3 Oct 2024 10:13:57 -0700 Subject: [PATCH 4/7] chore: added 200kb limit --- .../batch_sign_data_item.foreground.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts b/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts index 38525cb82..e0ba2c793 100644 --- a/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts +++ b/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts @@ -3,6 +3,8 @@ import type { ModuleFunction } from "~api/module"; import type { RawDataItem, SignDataItemParams } from "../sign_data_item/types"; import { isArrayBuffer } from "~utils/assertions"; +const MAX_TOTAL_SIZE = 200 * 1024; + const foreground: ModuleFunction[]> = async ( dataItems: SignDataItemParams[] ) => { @@ -10,6 +12,18 @@ const foreground: ModuleFunction[]> = async ( throw new Error("Input must be an array of data items"); } + const totalSize = dataItems.reduce((acc, dataItem) => { + const dataSize = + typeof dataItem.data === "string" + ? new TextEncoder().encode(dataItem.data).length + : dataItem.data.length; + return acc + dataSize; + }, 0); + + if (totalSize > MAX_TOTAL_SIZE) { + throw new Error("Total size of data items exceeds 100 KB"); + } + const rawDataItems: RawDataItem[] = dataItems.map((dataItem) => { let rawDataItem: RawDataItem; From 1d654ec774041c7850ea9892a695c33cf1b830d9 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 3 Oct 2024 10:15:31 -0700 Subject: [PATCH 5/7] fix: removed unused imports --- .../batch_sign_data_item.background.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts b/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts index b39a77cc5..600ebdf5e 100644 --- a/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts +++ b/src/api/modules/batch_sign_data_item/batch_sign_data_item.background.ts @@ -1,18 +1,11 @@ -import BigNumber from "bignumber.js"; import type { ModuleFunction } from "~api/module"; -import Application from "~applications/application"; import { isNotCancelError, isRawDataItem } from "~utils/assertions"; import authenticate from "../connect/auth"; import browser from "webextension-polyfill"; import { getActiveKeyfile } from "~wallets"; import { freeDecryptedWallet } from "~wallets/encryption"; -import Arweave from "arweave"; import { ArweaveSigner, createData, DataItem } from "arbundles"; - -interface RawDataItem { - data: number[]; - tags?: { name: string; value: string }[]; -} +import type { RawDataItem } from "../sign_data_item/types"; const background: ModuleFunction = async ( appData, From cb57b714552f918bd384a28043ae974a25298b81 Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Thu, 3 Oct 2024 10:28:27 -0700 Subject: [PATCH 6/7] fix: updated error messaging --- .../batch_sign_data_item/batch_sign_data_item.foreground.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts b/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts index e0ba2c793..094276e2b 100644 --- a/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts +++ b/src/api/modules/batch_sign_data_item/batch_sign_data_item.foreground.ts @@ -21,7 +21,7 @@ const foreground: ModuleFunction[]> = async ( }, 0); if (totalSize > MAX_TOTAL_SIZE) { - throw new Error("Total size of data items exceeds 100 KB"); + throw new Error("Total size of data items exceeds 200 KB"); } const rawDataItems: RawDataItem[] = dataItems.map((dataItem) => { From 9bcefd883643d2c0b4baf97bbb2b5f8fd6081cfc Mon Sep 17 00:00:00 2001 From: nicholas ma Date: Tue, 8 Oct 2024 13:21:18 -0700 Subject: [PATCH 7/7] fix: enabled auth popup on hardware wallets --- src/api/modules/sign/sign.background.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/modules/sign/sign.background.ts b/src/api/modules/sign/sign.background.ts index a52765c95..d1706233f 100644 --- a/src/api/modules/sign/sign.background.ts +++ b/src/api/modules/sign/sign.background.ts @@ -95,7 +95,7 @@ const background: ModuleFunction = async ( // check if there is an allowance limit, if there is we need to check allowance // if alwaysAsk is true, then we'll need to signAuth popup // if allowance is disabled, proceed with signing - if (alwaysAsk) { + if (alwaysAsk || activeWallet.type === "hardware") { // get address of keyfile const addr = activeWallet.type === "local"