Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat:ab name for disperse configuration #82

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 36 additions & 8 deletions packages/smol/components/Disperse/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import {cl, isAddress, toAddress} from '@builtbymom/web3/utils';
import {getClient} from '@builtbymom/web3/utils/wagmi';
import {SmolTokenSelector} from '@lib/common/SmolTokenSelector';
import {useAddressBook} from '@lib/contexts/useAddressBook';
import {usePrices} from '@lib/contexts/usePrices';
import {useValidateAddressInput} from '@lib/hooks/useValidateAddressInput';
import {useValidateAmountInput} from '@lib/hooks/useValidateAmountInput';
Expand Down Expand Up @@ -36,7 +37,7 @@
const {configuration, dispatchConfiguration} = useDisperse();
const {validate: validateAmount} = useValidateAmountInput();
const {chainID} = useChainID();

const {getEntry} = useAddressBook();
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>): void => {
if (!e.target.files) {
return;
Expand All @@ -44,100 +45,106 @@
const [file] = e.target.files as unknown as Blob[];
const reader = new FileReader();
set_isLoadingReceivers(true);
reader.onload = async event => {
if (!event?.target?.result) {
return;
}
const {result} = event.target;

/**************************************************************************************
** Parse the CSV file using Papa Parse
*************************************************************************************/
const parsedCSV = Papa.parse(result as string, {
header: true,
skipEmptyLines: true
});

/**************************************************************************************
** Check if the file is valid
*************************************************************************************/
const isValidFile =
parsedCSV.data.length > 0 &&
parsedCSV.meta.fields &&
parsedCSV.meta.fields.length === 2 &&
parsedCSV.meta.fields.includes('receiverAddress') &&
parsedCSV.meta.fields.includes('value');

if (isValidFile) {
/**************************************************************************************
** Extract field names
*************************************************************************************/
const [receiverAddress, value] = parsedCSV.meta.fields;

/**********************************************************************************
** Initialize an empty array for results
**********************************************************************************/
const records: TDisperseInput[] = [];

/**************************************************************************************
** Process each row to create records
*************************************************************************************/
const {length} = parsedCSV.data;
for (const row of parsedCSV.data) {
let abEntry;
const address = toAddress(row[receiverAddress]);
const amount = row[value];
let receiver = undefined;
if (length < 20) {
receiver = (await getAddressAndEns(row[receiverAddress], chainID)) as TAddressAndEns;
}

if (!isAddress(receiver?.address)) {
abEntry = await getEntry({label: row.receiverAddress});
} else {
abEntry = await getEntry({address: receiver?.address});
}
/**************************************************************************************
** Validate address and amount
*************************************************************************************/
if (receiver && isAddress(receiver?.address) && amount) {
const parsedAmount = parseFloat(amount).toString();

const record: TDisperseInput = {
receiver: {
address: receiver.address,
label: receiver.label ? receiver.label : receiver.address
address: receiver?.address || abEntry?.address,
label: abEntry ? abEntry.label : receiver.label ? receiver.label : receiver.address
} as TInputAddressLike,
value: {
...newDisperseVoidRow().value,
...validateAmount(parsedAmount, configuration.tokenToSend)
},
UUID: crypto.randomUUID()
};
records.push(record);
} else if (isAddress(address) && amount) {
const parsedAmount = parseFloat(amount).toString();

const record: TDisperseInput = {
receiver: {
address: toAddress(address),
label: toAddress(address)
} as TInputAddressLike,
value: {
...newDisperseVoidRow().value,
...validateAmount(parsedAmount, configuration.tokenToSend)
},
UUID: crypto.randomUUID()
};
records.push(record);
}
}
dispatchConfiguration({type: 'PASTE_RECEIVERS', payload: records});
} else {
console.error('The file you are trying to upload seems to be broken');
toast.error('The file you are trying to upload seems to be broken');
}
set_isLoadingReceivers(false);
};
reader.readAsText(file);
};

return (
<Button

Check notice on line 147 in packages/smol/components/Disperse/index.tsx

View check run for this annotation

codefactor.io / CodeFactor

packages/smol/components/Disperse/index.tsx#L48-L147

Complex Method
onClick={() => {
plausible(PLAUSIBLE_EVENTS.DISPERSE_IMPORT_CONFIG);
document.querySelector<HTMLInputElement>('#file-upload')?.click();
Expand Down Expand Up @@ -207,6 +214,7 @@
const {validate: validateAddress} = useValidateAddressInput();
const plausible = usePlausible();
const [isLoadingReceivers, set_isLoadingReceivers] = useState(false);
const {getEntry} = useAddressBook();

/**********************************************************************************************
** TODO: Add explanation of the downloadFile function
Expand Down Expand Up @@ -274,109 +282,128 @@
** Event listener when the user past something
*********************************************************************************************/
useEffect(() => {
const handlePaste = async (event: ClipboardEvent): Promise<void> => {
const {clipboardData} = event;
if (!clipboardData) {
return;
}
const text = clipboardData.getData('text');
if (!text) {
return;
}

/**************************************************************************************
** We want to continue ONLY if the content of the clipboard contains a valid pattern.
** The pattern is:
** 1. An address (0x followed by 40 characters)
** 2. A separator character (space, tab, comma, etc)
** 3. A number (integer or float)
** 4. A new line character (optional)
** 5. Repeat from step 1
** If the pattern is not found, we will ignore the content of the clipboard and return
** the function.
*************************************************************************************/
const trimedText = text.trim();
const pattern =
/^((?:0x[a-fA-F0-9]{40}|[a-zA-Z0-9-]+\.eth))[\s,;]+((?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\d+e[+-]?\d+)(?:\r?\n|$)/gm;

if (!pattern.test(trimedText)) {
toast.error(
'Invalid pattern. Please make sure that your clipboard contains a valid pattern: address, amount'
);
return;
}

/**************************************************************************************
** Split the text into lines and process each line
*************************************************************************************/
const lines = trimedText.split('\n');
const inputs: TDisperseInput[] = [];
set_isLoadingReceivers(true);
for (const line of lines) {
/**********************************************************************************
** Trim the line but preserve internal whitespace
*********************************************************************************/
const trimmedLine = line.trim();
if (trimmedLine === '') {
if (trimmedLine === '' || trimmedLine.split(',')[0] === 'receiverAddress') {
continue;
}

/**********************************************************************************
** Extract the address (first 42 characters starting with 0x) or the ens name.
*********************************************************************************/
const addressOrEnsMatch =
trimmedLine.match(/^(0x[a-fA-F0-9]{40})/) || trimmedLine.match(/^[a-zA-Z0-9-]+\.eth/);
trimmedLine.match(/^(0x[a-fA-F0-9]{40})/) ||
trimmedLine.match(/^[a-zA-Z0-9-]+\.eth/) ||
trimmedLine.match(/^((?!0x|\.|\d)[^\s\d]{1,22})/);

if (!addressOrEnsMatch) {
continue;
}
const [theAddressOrEns] = addressOrEnsMatch;
let fromAddressBook;

if (!theAddressOrEns.endsWith('.eth') && !theAddressOrEns.startsWith('0x')) {
fromAddressBook = await getEntry({label: theAddressOrEns});
}
if (theAddressOrEns.startsWith('0x')) {
fromAddressBook = await getEntry({address: toAddress(theAddressOrEns)});
}

/**********************************************************************************
** Validate the address
*********************************************************************************/
if (!isAddress(theAddressOrEns) && !theAddressOrEns.endsWith('.eth')) {
if (!isAddress(theAddressOrEns) && !theAddressOrEns.endsWith('.eth') && !fromAddressBook) {
continue;
}

const addressOrEns = await getAddressAndEns(theAddressOrEns, chainID);

/**********************************************************************************
** Extract the amount (everything after the address and any separators)
*********************************************************************************/
const amountPart = trimmedLine.slice(theAddressOrEns.length).trim();

/**********************************************************************************
** Find the first valid number in the remaining part
*********************************************************************************/
const theAmountMatch = amountPart.match(/((?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\d+e[+-]?\d+)/i);
const theAmount = theAmountMatch ? theAmountMatch[0] : '0';
const parsedAmount = parseFloat(theAmount).toString();

/**********************************************************************************
** Create the receiver object
*********************************************************************************/
const receiver = toAddress(theAddressOrEns);
const ensName = await getClient(1).getEnsName({address: receiver});
const label = addressOrEns?.label ? addressOrEns?.label : ensName ? ensName : toAddress(receiver);
const label = fromAddressBook
? fromAddressBook?.label || fromAddressBook?.ens
: addressOrEns?.label
? addressOrEns?.label
: ensName
? ensName
: toAddress(receiver);

const address = addressOrEns?.address ? addressOrEns?.address : fromAddressBook?.address;
const value = {
receiver: {
address: addressOrEns?.address ?? toAddress(receiver),
address,
label,
error: '',
isValid: 'undetermined',
source: 'typed'
} as TInputAddressLike,
value: {...newDisperseVoidRow().value, ...validateAmount(parsedAmount, configuration.tokenToSend)},
UUID: crypto.randomUUID()
};

inputs.push(value);
}
set_isLoadingReceivers(false);
dispatchConfiguration({type: 'PASTE_RECEIVERS', payload: inputs});
};

document.addEventListener('paste', handlePaste);

Check notice on line 406 in packages/smol/components/Disperse/index.tsx

View check run for this annotation

codefactor.io / CodeFactor

packages/smol/components/Disperse/index.tsx#L285-L406

Complex Method
return () => {
document.removeEventListener('paste', handlePaste);
};
Expand All @@ -385,6 +412,7 @@
configuration.inputs,
configuration.tokenToSend,
dispatchConfiguration,
getEntry,
validateAddress,
validateAmount
]);
Expand Down