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 426a0376..8655315f 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 @@ -46,18 +46,22 @@ const background: ModuleFunction = async ( ) ) { try { - const tags = dataItem?.tags || []; - const quantityTag = tags.find((tag) => tag.name === "Quantity"); + const quantityTag = dataItem.tags?.find((tag) => tag.name === "Quantity"); if (quantityTag) { - const quantity = BigNumber(quantityTag.value).toFixed( - 0, - BigNumber.ROUND_FLOOR - ); - if (!isNaN(+quantity)) { - quantityTag.value = quantity; + const quantityBigNum = BigNumber(quantityTag.value); + + // Ensure the quantity is a valid positive non-zero number (greater than 0) + if (!quantityBigNum.isPositive() || quantityBigNum.isZero()) { + throw new Error("INVALID_QUANTITY"); } + + quantityTag.value = quantityBigNum.toFixed(0, BigNumber.ROUND_FLOOR); } - } catch {} + } catch (e) { + if (e?.message === "INVALID_QUANTITY") { + throw new Error("Quantity must be a valid positive non-zero number."); + } + } try { await authenticate({ type: "signDataItem", diff --git a/src/components/popup/HistoryProvider.tsx b/src/components/popup/HistoryProvider.tsx index b6ddaac7..01f8e3b7 100644 --- a/src/components/popup/HistoryProvider.tsx +++ b/src/components/popup/HistoryProvider.tsx @@ -1,5 +1,5 @@ import { type PropsWithChildren, useEffect, useState } from "react"; -import { getDecryptionKey, isExpired } from "~wallets/auth"; +import { getDecryptionKey } from "~wallets/auth"; import { useLocation } from "wouter"; import { type BackAction, @@ -28,14 +28,12 @@ export default function HistoryProvider({ children }: PropsWithChildren<{}>) { }; // redirect to unlock if decryiption - // key is not available or if the password - // has expired and needs to be reset + // key is not available useEffect(() => { (async () => { const decryptionKey = await getDecryptionKey(); - const expired = await isExpired(); - if (!decryptionKey || expired) { + if (!decryptionKey) { push("/unlock"); } })(); diff --git a/src/components/popup/home/Balance.tsx b/src/components/popup/home/Balance.tsx index cfbc0831..4b0a6219 100644 --- a/src/components/popup/home/Balance.tsx +++ b/src/components/popup/home/Balance.tsx @@ -124,7 +124,9 @@ export default function Balance() { } useEffect(() => { - if (parseFloat(balance.toString()) !== historicalBalance[0]) { + if ( + balance.toNumber() !== historicalBalance[historicalBalance.length - 1] + ) { setLoading(true); } else { setLoading(false); @@ -198,13 +200,19 @@ export default function Balance() { async function balanceHistory(address: string, gateway: Gateway) { const arweave = new Arweave(gateway); + let minHeight = 0; + try { + const { height } = await arweave.network.getInfo(); + // blocks per day - 720 + minHeight = height - 720 * 30; + } catch {} // find txs coming in and going out const inTxs = ( await gql( ` - query($recipient: String!) { - transactions(recipients: [$recipient], first: 100) { + query($recipient: String!, $minHeight: Int!) { + transactions(recipients: [$recipient], first: 100, bundledIn: null, block: {min: $minHeight}) { edges { node { owner { @@ -224,14 +232,15 @@ async function balanceHistory(address: string, gateway: Gateway) { } } `, - { recipient: address } + { recipient: address, minHeight } ) ).data.transactions.edges; + const outTxs = ( await gql( ` - query($owner: String!) { - transactions(owners: [$owner], first: 100) { + query($owner: String!, $minHeight: Int!) { + transactions(owners: [$owner], first: 100, bundledIn: null, block: {min: $minHeight}) { edges { node { owner { @@ -251,38 +260,42 @@ async function balanceHistory(address: string, gateway: Gateway) { } } `, - { owner: address } + { owner: address, minHeight } ) ).data.transactions.edges; - // sort txs + // Merge and sort transactions in descending order (newest first) const txs = inTxs .concat(outTxs) .map((edge) => edge.node) - .filter((tx) => !!tx?.block?.timestamp) - .sort((a, b) => a.block.timestamp - b.block.timestamp); + .filter((tx) => !!tx?.block?.timestamp) // Filter out transactions without a timestamp + .sort((a, b) => b.block.timestamp - a.block.timestamp); // Sort by newest to oldest - // get initial balance - let balance = parseFloat( + // Get the current balance + let balance = BigNumber( arweave.ar.winstonToAr(await arweave.wallets.getBalance(address)) ); - const res = [balance]; + // Initialize the result array with the current balance + const res = [balance.toNumber()]; - // go back in time by tx and calculate - // historical balance + // Process transactions from newest to oldest, adjusting the balance for (const tx of txs) { - balance -= parseFloat(tx.fee.ar); - if (tx.owner.address === address) { - balance -= parseFloat(tx.quantity.ar); + // Outgoing transaction: add back the transaction amount and fee (since we are reversing) + balance = balance.plus(tx.quantity.ar).plus(tx.fee.ar); } else { - balance += parseFloat(tx.quantity.ar); + // Incoming transaction: subtract the amount received + balance = balance.minus(tx.quantity.ar); } - res.push(balance); + // Push the balance at that point in time + res.push(balance.toNumber()); } + // Reverse the result array to have chronological order for the line chart (oldest to newest) + res.reverse(); + return res; } diff --git a/src/components/popup/home/Transactions.tsx b/src/components/popup/home/Transactions.tsx index e4ea44b1..79ad7274 100644 --- a/src/components/popup/home/Transactions.tsx +++ b/src/components/popup/home/Transactions.tsx @@ -27,6 +27,7 @@ import { sortFn, type ExtendedTransaction } from "~lib/transactions"; +import BigNumber from "bignumber.js"; export default function Transactions() { const [transactions, fetchTransactions] = useState([]); @@ -62,8 +63,12 @@ export default function Transactions() { ) ); - const sent = await processTransactions(rawSent, "sent"); - const received = await processTransactions(rawReceived, "received"); + let sent = await processTransactions(rawSent, "sent"); + sent = sent.filter((tx) => BigNumber(tx.node.quantity.ar).gt(0)); + let received = await processTransactions(rawReceived, "received"); + received = received.filter((tx) => + BigNumber(tx.node.quantity.ar).gt(0) + ); const aoSent = await processTransactions(rawAoSent, "aoSent", true); const aoReceived = await processTransactions( rawAoReceived, diff --git a/src/lib/ao.ts b/src/lib/ao.ts index cfecb8e6..11ad24b3 100644 --- a/src/lib/ao.ts +++ b/src/lib/ao.ts @@ -66,9 +66,12 @@ export class AOProcess { this.processId = processId; this.ao = connect({ GRAPHQL_URL: - connectionConfig?.GATEWAY_URL ?? - joinUrl({ url: defaultConfig.GATEWAY_URL, path: "graphql" }), - CU_URL: connectionConfig?.GATEWAY_URL ?? defaultConfig.CU_URL, + connectionConfig?.GRAPHQL_URL ?? + joinUrl({ + url: connectionConfig?.GATEWAY_URL ?? defaultConfig.GATEWAY_URL, + path: "graphql" + }), + CU_URL: connectionConfig?.CU_URL ?? defaultConfig.CU_URL, MU_URL: connectionConfig?.MU_URL ?? defaultConfig.MU_URL, GATEWAY_URL: connectionConfig?.GATEWAY_URL ?? defaultConfig.GATEWAY_URL }); diff --git a/src/notifications/api.ts b/src/notifications/api.ts index d8b71143..5600b72c 100644 --- a/src/notifications/api.ts +++ b/src/notifications/api.ts @@ -14,6 +14,7 @@ import { combineAndSortTransactions, processTransactions } from "./utils"; +import BigNumber from "bignumber.js"; export type RawTransaction = { node: { @@ -87,13 +88,15 @@ export async function notificationsHandler(alarmInfo?: Alarms.Alarm) { query: !aoNotificationSetting.includes("allTxns") ? AR_RECEIVER_QUERY : ALL_AR_RECEIVER_QUERY, - variables: { address } + variables: { address }, + isAllTxns: aoNotificationSetting.includes("allTxns") }, { query: !aoNotificationSetting.includes("allTxns") ? AR_SENT_QUERY : ALL_AR_SENT_QUERY, - variables: { address } + variables: { address }, + isAllTxns: aoNotificationSetting.includes("allTxns") } ] ); @@ -181,7 +184,11 @@ const arNotificationsHandler = async ( address: string, lastStoredHeight: number, notificationSetting: boolean, - queriesConfig: { query: string; variables: Record }[] + queriesConfig: { + query: string; + variables: Record; + isAllTxns?: boolean; + }[] ): Promise => { try { let transactionDiff = []; @@ -189,7 +196,20 @@ const arNotificationsHandler = async ( const queries = queriesConfig.map((config) => gql(config.query, config.variables, suggestedGateways[1]) ); - const responses = await Promise.all(queries); + let responses = await Promise.all(queries); + responses = responses.map((response, index) => { + if ( + typeof queriesConfig[index].isAllTxns === "boolean" && + !queriesConfig[index].isAllTxns + ) { + response.data.transactions.edges = + response.data.transactions.edges.filter((edge) => + BigNumber(edge.node.quantity.ar).gt(0) + ); + } + return response; + }); + const combinedTransactions = combineAndSortTransactions(responses); const enrichedTransactions = processTransactions( diff --git a/src/notifications/utils.ts b/src/notifications/utils.ts index fd58d6c9..acf7033a 100644 --- a/src/notifications/utils.ts +++ b/src/notifications/utils.ts @@ -2,8 +2,7 @@ export const AR_RECEIVER_QUERY = ` query ($address: String!) { - transactions(first: 10, recipients: [$address], tags: [{ name: "Type", values: ["Transfer"] }]) { - + transactions(first: 10, recipients: [$address], bundledIn: null) { edges { cursor node { @@ -23,7 +22,7 @@ query ($address: String!) { `; export const AR_SENT_QUERY = `query ($address: String!) { - transactions(first: 10, owners: [$address], tags: [{ name: "Type", values: ["Transfer"] }]) { + transactions(first: 10, owners: [$address], bundledIn: null) { edges { cursor node { @@ -136,7 +135,7 @@ export const ALL_AR_SENT_QUERY = `query ($address: String!) { export const AR_RECEIVER_QUERY_WITH_CURSOR = ` query ($address: String!, $after: String) { - transactions(first: 10, recipients: [$address], tags: [{ name: "Type", values: ["Transfer"] }], after: $after) { + transactions(first: 10, recipients: [$address], bundledIn: null, after: $after) { pageInfo { hasNextPage } @@ -160,7 +159,7 @@ query ($address: String!, $after: String) { export const AR_SENT_QUERY_WITH_CURSOR = ` query ($address: String!, $after: String) { - transactions(first: 10, owners: [$address], tags: [{ name: "Type", values: ["Transfer"] }], after: $after) { + transactions(first: 10, owners: [$address], bundledIn: null, after: $after) { pageInfo { hasNextPage } diff --git a/src/routes/popup/index.tsx b/src/routes/popup/index.tsx index 84e0fe08..af6fbd35 100644 --- a/src/routes/popup/index.tsx +++ b/src/routes/popup/index.tsx @@ -5,7 +5,7 @@ import WalletHeader from "~components/popup/WalletHeader"; import NoBalance from "~components/popup/home/NoBalance"; import Balance from "~components/popup/home/Balance"; import { AnnouncementPopup } from "./announcement"; -import { getDecryptionKey, isExpired } from "~wallets/auth"; +import { getDecryptionKey } from "~wallets/auth"; import { useHistory } from "~utils/hash_router"; import { trackEvent, @@ -87,19 +87,11 @@ export default function Home() { }, [activeAddress, assets, aoTokens]); useEffect(() => { - // check if password is expired here - const checkExpiration = async () => { - const expired = await isExpired(); - // delete expiration from storage here or in unlock page - if (expired) { - ExtensionStorage.remove("password_expires"); - push("/unlock"); - } else { - await trackEvent(EventType.LOGIN, {}); - await trackPage(PageType.HOME); - } + const trackEventAndPage = async () => { + await trackEvent(EventType.LOGIN, {}); + await trackPage(PageType.HOME); }; - checkExpiration(); + trackEventAndPage(); // schedule import ao tokens scheduleImportAoTokens(); diff --git a/src/routes/popup/transaction/[id].tsx b/src/routes/popup/transaction/[id].tsx index 47700867..2475295e 100644 --- a/src/routes/popup/transaction/[id].tsx +++ b/src/routes/popup/transaction/[id].tsx @@ -46,9 +46,7 @@ import { TempTransactionStorage } from "~utils/storage"; import { useContact } from "~contacts/hooks"; import { EventType, PageType, trackEvent, trackPage } from "~utils/analytics"; import BigNumber from "bignumber.js"; -import { Token } from "ao-tokens"; import { fetchTokenByProcessId } from "~lib/transactions"; -import type { TokenInfo } from "~tokens/aoTokens/ao"; // pull contacts and check if to address is in contacts @@ -161,11 +159,9 @@ export default function Transaction({ id: rawId, gw, message }: Props) { ); if (aoQuantity) { - let tokenInfo; - tokenInfo = await fetchTokenByProcessId(data.transaction.recipient); - if (!tokenInfo) { - tokenInfo = (await Token(data.transaction.recipient)).info; - } + const tokenInfo = await fetchTokenByProcessId( + data.transaction.recipient + ); if (tokenInfo) { const amount = balanceToFractioned(aoQuantity.value, { id: data.transaction.recipient, diff --git a/src/routes/popup/transaction/transactions.tsx b/src/routes/popup/transaction/transactions.tsx index b476b310..e54a3c55 100644 --- a/src/routes/popup/transaction/transactions.tsx +++ b/src/routes/popup/transaction/transactions.tsx @@ -29,6 +29,7 @@ import { getFullMonthName, getTransactionDescription } from "~lib/transactions"; +import BigNumber from "bignumber.js"; const defaultCursors = ["", "", "", ""]; const defaultHasNextPages = [true, true, true, true]; @@ -85,8 +86,8 @@ export default function Transactions() { }) ); - const sent = await processTransactions(rawSent, "sent"); - const received = await processTransactions(rawReceived, "received"); + let sent = await processTransactions(rawSent, "sent"); + let received = await processTransactions(rawReceived, "received"); const aoSent = await processTransactions(rawAoSent, "aoSent", true); const aoReceived = await processTransactions( rawAoReceived, @@ -100,6 +101,9 @@ export default function Transactions() { ) ); + sent = sent.filter((tx) => BigNumber(tx.node.quantity.ar).gt(0)); + received = received.filter((tx) => BigNumber(tx.node.quantity.ar).gt(0)); + setHasNextPages( [rawReceived, rawSent, rawAoSent, rawAoReceived].map( (result) => diff --git a/src/routes/popup/unlock.tsx b/src/routes/popup/unlock.tsx index ea96c644..8edee3c1 100644 --- a/src/routes/popup/unlock.tsx +++ b/src/routes/popup/unlock.tsx @@ -1,18 +1,7 @@ -import PasswordStrength from "~components/welcome/PasswordStrength"; -import PasswordMatch from "~components/welcome/PasswordMatch"; -import { checkPasswordValid } from "~wallets/generator"; -import { useEffect, useMemo, useState } from "react"; -import { - addExpiration, - checkPassword, - isExpired, - removeDecryptionKey, - unlock -} from "~wallets/auth"; +import { unlock } from "~wallets/auth"; import { useHistory } from "~utils/hash_router"; import Wrapper from "~components/auth/Wrapper"; import browser from "webextension-polyfill"; -import { updatePassword } from "~wallets"; import { ButtonV2, InputV2, @@ -20,46 +9,21 @@ import { Spacer, Text, useInput, - useModal, useToasts } from "@arconnect/components"; import HeadV2 from "~components/popup/HeadV2"; import styled from "styled-components"; -import { PasswordWarningModal } from "./passwordPopup"; -import { passwordStrength } from "check-password-strength"; export default function Unlock() { - const [expired, setExpired] = useState(false); // password input const passwordInput = useInput(); - const newPasswordInput = useInput(); - const confirmNewPasswordInput = useInput(); // toasts const { setToast } = useToasts(); - // check expiry - useEffect(() => { - const checkExpiration = async () => { - const expired = await isExpired(); - setExpired(expired); - }; - checkExpiration(); - }, []); - // router push const [push] = useHistory(); - const passwordModal = useModal(); - - const passwordStatus = passwordStrength(newPasswordInput.state); - - async function lockWallet() { - await removeDecryptionKey(); - setExpired(false); - push("/unlock"); - } - // unlock ArConnect async function unlockWallet() { // unlock using password @@ -77,77 +41,6 @@ export default function Unlock() { push("/"); } - // changes password and unlock ArConnect - async function changeAndUnlock(skip: boolean = false) { - if (newPasswordInput.state !== confirmNewPasswordInput.state) { - return setToast({ - type: "error", - content: browser.i18n.getMessage("passwords_not_match"), - duration: 2300 - }); - } - - if (newPasswordInput.state === passwordInput.state) { - // also need to verify that passwordInput is valid - const res = await unlock(passwordInput.state); - - if (!res) { - passwordInput.setStatus("error"); - return setToast({ - type: "error", - content: browser.i18n.getMessage("invalidPassword"), - duration: 2200 - }); - } - - return setToast({ - type: "error", - content: browser.i18n.getMessage("passwords_match_previous"), - duration: 2300 - }); - } - - if (!(await checkPassword(passwordInput.state))) { - return setToast({ - type: "error", - content: browser.i18n.getMessage("invalidPassword"), - duration: 2200 - }); - } - - // check password validity - if (!checkPasswordValid(newPasswordInput.state) && !skip) { - passwordModal.setOpen(true); - return; - } - - try { - await updatePassword(newPasswordInput.state, passwordInput.state); - await unlock(newPasswordInput.state); - - push("/"); - } catch { - return setToast({ - type: "error", - content: browser.i18n.getMessage("invalidPassword"), - duration: 2200 - }); - } - } - - // password valid - const validPassword = useMemo( - () => checkPasswordValid(newPasswordInput.state), - [newPasswordInput] - ); - - // passwords match - const matches = useMemo( - () => - newPasswordInput.state === confirmNewPasswordInput.state && validPassword, - [newPasswordInput, confirmNewPasswordInput, validPassword] - ); - return (
@@ -160,24 +53,9 @@ export default function Unlock() {
- {browser.i18n.getMessage( - expired ? "reset_wallet_password_to_use" : "unlock_wallet_to_use" - )} + {browser.i18n.getMessage("unlock_wallet_to_use")} - {expired && ( - { - await addExpiration(); - await lockWallet(); - }} - > - {browser.i18n.getMessage("password_change")} - - )} - - { - if (e.key !== "Enter" || expired) return; + if (e.key !== "Enter") return; unlockWallet(); }} autoFocus /> - {expired && ( - <> - - - - { - if (e.key !== "Enter") return; - changeAndUnlock(); - }} - /> - - - - - )}
- changeAndUnlock() : unlockWallet} - > + {browser.i18n.getMessage("unlock")}
-
); } - -const AltText = styled(Text)` - font-size: 12px; - color: rgb(${(props) => props.theme.primaryText}); - text-decoration: underline; - cursor: pointer; -`; diff --git a/src/routes/welcome/generate/done.tsx b/src/routes/welcome/generate/done.tsx index 89f2a93e..ef329871 100644 --- a/src/routes/welcome/generate/done.tsx +++ b/src/routes/welcome/generate/done.tsx @@ -18,7 +18,6 @@ import { ExtensionStorage } from "~utils/storage"; import { useStorage } from "@plasmohq/storage/hook"; import JSConfetti from "js-confetti"; import { useLocation } from "wouter"; -import { addExpiration } from "~wallets/auth"; export default function Done() { // wallet context @@ -83,9 +82,6 @@ export default function Done() { password ); - // add password expiration - await addExpiration(); - // log user onboarded await trackEvent(EventType.ONBOARDED, {}); diff --git a/src/routes/welcome/load/wallets.tsx b/src/routes/welcome/load/wallets.tsx index 2b579101..dab1e310 100644 --- a/src/routes/welcome/load/wallets.tsx +++ b/src/routes/welcome/load/wallets.tsx @@ -27,7 +27,6 @@ import SeedInput from "~components/SeedInput"; import Paragraph from "~components/Paragraph"; import browser from "webextension-polyfill"; import styled from "styled-components"; -import { addExpiration } from "~wallets/auth"; import { WalletKeySizeErrorModal } from "~components/modals/WalletKeySizeErrorModal"; export default function Wallets() { @@ -165,7 +164,6 @@ export default function Wallets() { // add wallet await addWallet(jwk, password); - await addExpiration(); } else if (existingWallets.length < 1) { // the user has not migrated, so they need to add a wallet return finishUp(); @@ -191,7 +189,6 @@ export default function Wallets() { // we need this because we don't // have any other wallets added yet await setActiveWallet(account.address); - await addExpiration(); // redirect setLocation(`/${params.setup}/${Number(params.page) + 1}`); diff --git a/src/wallets/auth.ts b/src/wallets/auth.ts index 715953eb..fe7787cf 100644 --- a/src/wallets/auth.ts +++ b/src/wallets/auth.ts @@ -3,12 +3,6 @@ import browser, { type Alarms } from "webextension-polyfill"; import { getWallets, type LocalWallet } from "./index"; import { ExtensionStorage } from "~utils/storage"; -/** - * Name of the store that holds the expiration date - * for the current password. - */ -export const EXPIRATION_STORAGE = "password_expires"; - /** * Unlock wallets and save decryption key * @@ -28,9 +22,6 @@ export async function unlock(password: string) { // schedule the key for removal await scheduleKeyRemoval(); - // add expiration if needed - await addExpiration(); - return true; } @@ -84,16 +75,6 @@ export async function getDecryptionKey() { return atob(val); } - -export async function isExpired() { - const val = await ExtensionStorage.get(EXPIRATION_STORAGE); - - // expired - if (Date.now() > val || !val) { - return true; - } -} - /** * Set wallet decryption key * @@ -151,28 +132,3 @@ export async function onWindowClose() { // remove the decryption key await removeDecryptionKey(); } - -/** - * Add password expiration date, if it is - * not in the extension storage yet. - */ -export async function addExpiration() { - // add expiration date for password if not present - let expires = await ExtensionStorage.get(EXPIRATION_STORAGE); - - if (!expires) { - const newExpiration = new Date(); - - // set expiration date in 6 months - newExpiration.setMonth(newExpiration.getMonth() + 6); - expires = newExpiration.getTime(); - - // set value - await ExtensionStorage.set(EXPIRATION_STORAGE, expires); - - // schedule session reset once the password expired - browser.alarms.create("remove_decryption_key_scheduled", { - when: expires - }); - } -} diff --git a/src/wallets/index.ts b/src/wallets/index.ts index d572f8ee..0655bb0c 100644 --- a/src/wallets/index.ts +++ b/src/wallets/index.ts @@ -13,13 +13,7 @@ import { encryptWallet, freeDecryptedWallet } from "./encryption"; -import { - addExpiration, - checkPassword, - EXPIRATION_STORAGE, - getDecryptionKey, - setDecryptionKey -} from "./auth"; +import { checkPassword, getDecryptionKey, setDecryptionKey } from "./auth"; import { ArweaveSigner } from "arbundles"; /** @@ -344,11 +338,7 @@ export async function updatePassword( item.keyfile = encrypted; } - // remove previous expiration data - await ExtensionStorage.remove(EXPIRATION_STORAGE); - // update state - await addExpiration(); await setDecryptionKey(newPassword); await ExtensionStorage.set("wallets", wallets); }