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 (
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 (
);
};
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 (
{
mixpanel.track('Simulation Rejected', {
id,
state,
});
log.info({ id, state }, 'Simulation Rejected');
updateSimulationState(id, StoredSimulationState.Rejected);
}}
>
Reject
{
mixpanel.track('Simulation Continue', {
id,
state,
});
log.info({ id, state }, 'Simulation Continue');
updateSimulationState(id, StoredSimulationState.Confirmed);
}}
>
{state === StoredSimulationState.Success ||
state === StoredSimulationState.Revert
? 'Continue'
: 'Skip'}
);
}
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 (
);
}
if (
storedSimulation.state === StoredSimulationState.Revert &&
storedSimulation?.type === StoredType.Signature
) {
return (
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 (
Simulation shows the transaction will fail
{storedSimulation.error &&
` with message ${JSON.stringify(
storedSimulation.error,
null,
2
)}.`}
);
}
if (storedSimulation.state === StoredSimulationState.Error) {
return (
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 && (
)}
{
navigator.clipboard.writeText(toAddress || '');
setCopyText(COPIED_TEXT);
// Revert after 2 second.
setTimeout(() => {
setCopyText(COPY_TEXT);
}, 2000);
}}
>
copyText}
/>
{toAddress}
);
};
// TODO: handle the TO address separately.
if (storedSimulation.state === StoredSimulationState.Success) {
return (
);
}
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;