import * as Sentry from '@sentry/react'; import { utils, BigNumber } from 'ethers'; import mixpanel from 'mixpanel-browser'; import React from 'react'; import { MdVerified } from 'react-icons/md'; import { AiFillCopy } from 'react-icons/ai'; import { BeatLoader } from 'react-spinners'; import { useState, useEffect } from 'react'; import ReactTooltip from 'react-tooltip'; import browser from 'webextension-polyfill'; import logger from '../../lib/logger'; import { Simulation, Event, EventType, TokenType } from '../../lib/models'; import type { StoredSimulation } from '../../lib/storage'; import { StoredType } from '../../lib/storage'; import { STORAGE_KEY, simulationNeedsAction, StoredSimulationState, updateSimulationState, } from '../../lib/storage'; const log = logger.child({ component: 'Popup' }); Sentry.init({ dsn: 'https://e130c8dff39e464bab4c609c460068b0@o1317041.ingest.sentry.io/6569982', }); mixpanel.init('00d3b8bc7c620587ecb1439557401a87'); const NoTransactionComponent = () => { return (
rocket taking off
Trigger a transaction to get started.
); }; /** * Pass in a hex string, get out the parsed amount. * * If the amount is undefined or null, the return will be 1. */ const getFormattedAmount = ( amount: string | null, decimals: number | null ): string => { if (!amount) { return '1'; } if (!decimals) { decimals = 0; } if (amount === '0x') { return '0'; } const amountParsed = BigNumber.from(amount); // We're okay to round here a little bit since we're just formatting. const amountAsFloatEther = parseFloat( utils.formatUnits(amountParsed, decimals) ); let formattedAmount; if (amountAsFloatEther > 1 && amountAsFloatEther % 1 !== 0) { // Add 4 decimals if it is > 1 formattedAmount = amountAsFloatEther.toFixed(4); } else { // Add precision of 4. formattedAmount = amountAsFloatEther.toLocaleString('fullwide', { useGrouping: false, maximumSignificantDigits: 4, }); } return formattedAmount; }; const EventComponent = ({ event }: { event: Event }) => { const formattedAmount = getFormattedAmount(event.amount, event.decimals); const message = () => { if ( event.type === EventType.TransferIn || event.type === EventType.TransferOut ) { return (
{event.type === EventType.TransferIn ? '+' : '-'} {formattedAmount}{' '} {event.tokenType === TokenType.ERC721 ? 'NFT' : event.name}
); } if (event.type === EventType.Approval) { const color = event.verifiedAddressName ? 'text-gray-100' : 'text-red-500'; return (
Permission to withdraw{' '} {event.tokenType !== TokenType.ERC721 && `${formattedAmount} ${event.name}`}
); } if (event.type === EventType.ApprovalForAll) { const color = event.verifiedAddressName ? 'text-gray-100' : 'text-red-500'; return (
Can withdraw
{event.tokenType === TokenType.ERC721 ? (
ALL NFTs{' '} from {event.name}.
) : (
ALL {event.name} tokens.
)}
); } }; return (
token
{event.name || 'Unknown Name'}
{event.verified && (
)}
{message()}
); }; const PotentialWarnings = ({ simulation, type, verified, }: { simulation: Simulation; type: StoredType; verified: boolean; }) => { const events = simulation.events; // Should be protected against this, no events should show no change in assets. if (events.length === 0) { return null; } const event = events[0]; if (type === StoredType.Simulation) { const NoApprovalForAll = (
Changes being made in this transaction
); // Show the warning for non-verified addresses. if (event.type === EventType.ApprovalForAll && !verified) { return (
🚨 WARNING 🚨
You are giving approval to withdraw all.
Please make sure it is not a wallet drainer.
); } return (
{simulation.mustWarn && (
🚨 WARNING 🚨
{simulation.mustWarnMessage}
)}
{NoApprovalForAll}
); } else { const PotentialChangesMessage = (
Changes that can be made by signing this message
); return (
{simulation.shouldWarn && (
🚨 WARNING 🚨
{simulation.mustWarnMessage || 'Please make sure this is not a scam!'}
)}
{PotentialChangesMessage}
); } }; const SimulationComponent = ({ simulation }: { simulation: Simulation }) => { const simulationEvents = () => { if (simulation.events.length === 0) { return (
No changes in assets found!
); } return (
{simulation.events.map((event: any, index: number) => { return ; })}
); }; return
{simulationEvents()}
; }; const ConfirmSimulationButton = ({ id, state, }: { id: string; state: StoredSimulationState; }) => { if (simulationNeedsAction(state)) { return (
); } return null; }; const StoredSimulationComponent = ({ storedSimulation, }: { storedSimulation: StoredSimulation; }) => { const COPY_TEXT = 'Copy to clipboard'; const COPIED_TEXT = 'Copied!'; const [copyText, setCopyText] = useState(COPY_TEXT); if (storedSimulation.state === StoredSimulationState.Simulating) { return (
robot
Simulating
); } if ( storedSimulation.state === StoredSimulationState.Revert && storedSimulation?.type === StoredType.Signature ) { return (
failed
Please make sure you trust this website before continuing.
We could not decode the message. Let us know in Discord if you would like us to support this website/protocol.
); } if (storedSimulation.state === StoredSimulationState.Revert) { return (
failed
Simulation shows the transaction will fail {storedSimulation.error && ` with message ${JSON.stringify( storedSimulation.error, null, 2 )}.`}
); } if (storedSimulation.state === StoredSimulationState.Error) { return (
failure
Simulation could not be ran{' '} {storedSimulation.error && `with message ${JSON.stringify( storedSimulation.error, null, 2 )}.`}
Please contact the team for support (id: {storedSimulation.id}).
); } // Re-hydrate the functions. const simulation = Simulation.fromJSON(storedSimulation.simulation); let toAddress = simulation.toAddress; let verifiedAddressName = simulation.verifiedAddressName; let interactionText = 'Interacting with'; let approval = false; for (const event of simulation.events) { // Approval + ApprovalForAll we don't care about the interacting contract, rather we only care about the toAddress. // // There should only be 1 of these events. if ( event.type === EventType.Approval || event.type === EventType.ApprovalForAll ) { approval = true; interactionText = 'Giving approval to'; toAddress = event.toAddress; verifiedAddressName = event.verifiedAddressName; } } // toAddress must always be set for non signature simulations. const interactingAddress = () => { // Don't show anything for non-approval signatures. // That is we still show permits and who it is to. if (!approval && storedSimulation.type === StoredType.Signature) { return null; } return (
{interactionText}
{verifiedAddressName && (
{verifiedAddressName}
)}
); }; // TODO: handle the TO address separately. if (storedSimulation.state === StoredSimulationState.Success) { return (
{interactingAddress()}
); } return null; }; const TransactionComponent = () => { // TODO(jqphu): handle errors? // Storage mapping to StoredSimulation[] const [storedSimulations, setStoredSimulations] = useState< StoredSimulation[] >([]); console.log('STORED SIMS', storedSimulations); useEffect(() => { browser.storage.sync.get(STORAGE_KEY).then(({ simulations }) => { setStoredSimulations(simulations); }); browser.storage.onChanged.addListener((changes, area) => { if (area === 'sync' && changes[STORAGE_KEY]?.newValue) { const newSimulations = changes[STORAGE_KEY]?.newValue; setStoredSimulations(newSimulations); } }); }, []); const filteredSimulations = storedSimulations?.filter( (simulation: StoredSimulation) => simulation.state !== StoredSimulationState.Rejected && simulation.state !== StoredSimulationState.Confirmed ); if (!filteredSimulations || filteredSimulations.length === 0) { return (
); } return (
{filteredSimulations.length !== 1 && (
{filteredSimulations.length - 1} queued
)}
); }; export default TransactionComponent;