SendModal.tsx
1 import React, {FC, useRef, useState, useEffect } from "react"; 2 import * as Dialog from "@radix-ui/react-dialog"; 3 import { Button } from "@radix-ui/themes"; 4 import { Cross2Icon } from "@radix-ui/react-icons"; 5 import styles from "./sendmodal.module.scss"; 6 import { AccountRecordsDB, TransactionDB } from "../Database/Database"; 7 import { useLiveQuery } from "dexie-react-hooks"; 8 import { Principal } from "@dfinity/principal"; 9 import useAuth from "@/auth/hooks/useAuth"; 10 import { convertToBigInt } from "./utils/convertToBigInt"; 11 12 interface SendFormProps { 13 open: any, 14 onClose: any, 15 selectedAccount: any, 16 } 17 18 const SendModal: FC<SendFormProps> = ({ open, onClose, selectedAccount }) => { 19 const dialogRef = useRef(null); 20 const [destination, setDestination] = useState(""); 21 const [amount, setAmount] = useState(null); 22 const [inputAmount, setInputAmount] = useState(null); 23 const [usdAmount, setUsdAmount] = useState(null); 24 const [usdBalance, setUsdBalance] = useState(null); 25 const [ethExchangeRate] = useState(2513.62); 26 const [eurcExchangeRate] = useState(1.09); 27 const [latestBalance, setLatestBalance] = useState(null); 28 const [updatedBalance, setUpdatedBalance] = useState(null); 29 const [selectedDestinationAccount, setSelectedDestinationAccount] = useState(null); 30 const [senderUpdatedUsdBalance, setSenderUpdatedUsdBalance] = useState(null); 31 const [receiverUsdBalance, setReceiverUsdBalance] = useState(null); 32 const [ckBtcBalance, setCkBtcBalance] = useState<bigint | null | undefined>(); 33 const { principal, ledgerActor } = useAuth(); 34 35 const allAccountRecordsData = useLiveQuery( 36 () => { 37 return AccountRecordsDB.accountRecordsDetails 38 .orderBy('timestamp') 39 .reverse() 40 .toArray() 41 .then(records => { 42 const uniqueRecords = records.reduce((unique, record) => { 43 if (!unique.some(r => r.accountName === record.accountName)) { 44 unique.push(record); 45 } 46 return unique; 47 }, []); 48 return uniqueRecords; 49 }); 50 }, 51 [], 52 ); 53 54 const newBalance = useLiveQuery( 55 async () => { 56 if (selectedAccount && selectedAccount.accountName) { 57 const result = await AccountRecordsDB.accountRecordsDetails 58 .where('accountName') 59 .equals(selectedAccount.accountName) 60 .last(); 61 return result; 62 } else { 63 return null; 64 } 65 }, 66 [selectedAccount, amount], 67 ); 68 69 useEffect(() => { 70 const fetchNewBalance = async () => { 71 try { 72 const newBalanceResult = await newBalance; 73 if (newBalanceResult) { 74 console.log("New balance result object: ", newBalanceResult); 75 console.log("Your new balance is: ", parseFloat(newBalanceResult.nativeBalance)); 76 setLatestBalance(newBalanceResult.nativeBalance); 77 } else { 78 setLatestBalance(selectedAccount?.balance); 79 } 80 } catch (error) { 81 console.error("Error updating balance:", error); 82 } 83 }; 84 85 fetchNewBalance(); 86 }, [newBalance, selectedAccount, amount]); 87 88 89 const handleAmountChange = (event) => { 90 event.preventDefault(); 91 const newInputAmount = event.target.value; 92 setInputAmount(newInputAmount); 93 setAmount(null); 94 }; 95 96 97 const fetchSelectedDestinationAccountAmount = () => { 98 if (selectedDestinationAccount) { 99 console.log("Selected destination: ", selectedDestinationAccount); 100 setAmount(selectedDestinationAccount?.offerAmount || null); 101 } else { 102 console.log("Destination account has not been selected hence can't find destination amount"); 103 } 104 }; 105 106 useEffect(() => { 107 fetchSelectedDestinationAccountAmount(); 108 }, [selectedDestinationAccount, inputAmount]); 109 110 111 112 const handleDestinationChange = (event) => { 113 event.preventDefault(); 114 const selectedAccountName = event.target.value; 115 console.log("Selected account Id: ", selectedAccountName); 116 const selectedAccount = allAccountRecordsData.find(account => { 117 return String(account.accountName) === selectedAccountName; 118 }); 119 console.log("Selected account destination account: ", selectedAccount); 120 setSelectedDestinationAccount(selectedAccount); 121 setAmount(null); 122 }; 123 124 125 const dollarAmount = () => { 126 if (selectedDestinationAccount?.currency === "ETH") { 127 const calculatedAmount = inputAmount ? inputAmount : amount * ethExchangeRate; 128 setUsdAmount(calculatedAmount); 129 } else if (selectedDestinationAccount?.currency === "EURC") { 130 const calculatedAmount = inputAmount ? inputAmount : amount * eurcExchangeRate; 131 setUsdAmount(calculatedAmount); 132 } else { 133 setUsdAmount(inputAmount || amount); 134 } 135 }; 136 137 const dollarBalance = () => { 138 if (selectedAccount?.currency === "ETH") { 139 const dollarBalance = latestBalance * ethExchangeRate; 140 console.log("Latest balance in send: ", latestBalance); 141 console.log("Usd Balance in send: ", dollarBalance); 142 setUsdBalance(dollarBalance); 143 } else if (selectedAccount?.currency === "EURC") { 144 const dollarBalance = latestBalance * eurcExchangeRate; 145 setUsdBalance(dollarBalance); 146 } else { 147 setUsdBalance(latestBalance || 0); 148 } 149 }; 150 151 useEffect(() => { 152 if (selectedAccount) { 153 dollarAmount(); 154 dollarBalance(); 155 } 156 }, [inputAmount, latestBalance, selectedDestinationAccount]); 157 158 159 const handleClose = () => { 160 if (onClose) { 161 onClose(); 162 } 163 console.log("Selected Account arrived send component: ", selectedAccount); 164 }; 165 166 167 const updateAccountRecordsSenderData = async () => { 168 try { 169 const accountName = selectedAccount?.accountName; 170 console.log("Account name: ", accountName); 171 172 const senderNativeUpdatedBalance = latestBalance - amount 173 console.log("Sender native updated balance: ", senderNativeUpdatedBalance); 174 const senderUpdatedUsdBalance = parseFloat(usdBalance - usdAmount); 175 console.log("Sender updated usd balance: ", senderUpdatedUsdBalance); 176 177 const latestUniqueRecord = await AccountRecordsDB.accountRecordsDetails 178 .where('accountName') 179 .equals(accountName) 180 .last() 181 182 if(latestUniqueRecord) { 183 const accountKey = latestUniqueRecord.accountName 184 console.log("Account key: ", accountKey); 185 await AccountRecordsDB.accountRecordsDetails.update(accountKey,{ 186 state: "Escrow", 187 sentAmount: usdAmount, 188 nativeBalance: senderNativeUpdatedBalance, 189 usdBalance: senderUpdatedUsdBalance, 190 timestamp: new Date(), 191 }); 192 } else { 193 console.log("No existing records found for the specified account"); 194 } 195 console.log(`Succesfully ${accountName} changed state to Purchase`) 196 setUpdatedBalance(updatedBalance); 197 } catch (error) { 198 console.log("Could update to the account records database with sender records: ", error); 199 } 200 } 201 202 const getReceiverUsdBalance = () => { 203 if (selectedDestinationAccount?.currency === "ETH") { 204 const receiverDollarBalance = selectedDestinationAccount.balance * ethExchangeRate; 205 setReceiverUsdBalance(receiverDollarBalance); 206 } else if (selectedDestinationAccount?.currency === "EURC") { 207 const receiverDollarBalance = selectedDestinationAccount.balance * eurcExchangeRate; 208 setReceiverUsdBalance(receiverDollarBalance); 209 } else { 210 setReceiverUsdBalance(selectedDestinationAccount?.balance || 0); 211 } 212 }; 213 214 useEffect(()=> { 215 getReceiverUsdBalance(); 216 },[selectedDestinationAccount]); 217 218 const updateAccountRecordsReceiverData = async () => { 219 try { 220 const accountName = selectedDestinationAccount?.accountName; 221 console.log("Desination account name: ", accountName); 222 223 const receiverUpdatedNativeBalance = selectedDestinationAccount?.balance - amount 224 console.log(" Receiver updated native balance: ", receiverUpdatedNativeBalance); 225 const receiverUpdatedUsdBalance = receiverUsdBalance + usdAmount; 226 console.log("Receiver updated usd balance: ", receiverUpdatedUsdBalance); 227 228 const latestUniqueRecord = await AccountRecordsDB.accountRecordsDetails 229 .where('accountName') 230 .equals(accountName) 231 .last() 232 233 if(latestUniqueRecord) { 234 const accountKey = latestUniqueRecord.accountName; 235 await AccountRecordsDB.accountRecordsDetails.update(accountKey,{ 236 state: "Escrow", 237 offerAmount: usdAmount, 238 receivedAmount: usdAmount, 239 nativeBalance: receiverUpdatedNativeBalance, 240 usdBalance: receiverUpdatedUsdBalance, 241 timestamp: new Date(), 242 }); 243 } else { 244 console.log("No existing records found for the specified account") 245 } 246 console.log(`Succesfully ${accountName} changed state to Purchase`) 247 setUpdatedBalance(updatedBalance); 248 } catch(error) { 249 console.log("Could update to the account records database with receiver records: ", error) 250 } 251 } 252 253 useEffect(()=> { 254 getCkBtcBalance(); 255 },[]); 256 257 const getCkBtcBalance = async () => { 258 if(ledgerActor && principal ) { 259 const ownerPrincipal = Principal.fromText(principal); 260 console.log("Owner principal: ", ownerPrincipal); 261 try { 262 const res = await ledgerActor?.balance({ 263 owner: ownerPrincipal, 264 certified: false, 265 }); 266 console.log("cKBtc Balance: ", res); 267 setCkBtcBalance(res); 268 } catch(error) { 269 console.log("There was an issue fethcing the balance: ", error); 270 } 271 } else { 272 console.log("Make sure ledgerActor and principal are intitialized first"); 273 } 274 } 275 276 const handleTransferCkBtc = () => { 277 console.log("Destination:", selectedDestinationAccount?.sourcePrincipal); 278 console.log("Amount:", amount) 279 280 if(!ledgerActor) return; 281 282 let destinationPrincipal: Principal = Principal.fromText(''); 283 284 try { 285 destinationPrincipal = Principal.fromText(selectedDestinationAccount?.sourcePrincipal); 286 } catch(error) { 287 console.log("Invalid destination principal") 288 } 289 290 const _amount = Number.parseFloat(amount); 291 if(Number.isNaN(_amount)) { 292 console.log("Amount must be a number"); 293 } 294 if(_amount < 0) { 295 console.log("Amount must be greater than 0") 296 } 297 298 let amountBigInt: bigint = BigInt(0); 299 300 try { 301 amountBigInt = convertToBigInt(_amount) 302 } catch(error) { 303 console.log("Could not convert amount to bigInt") 304 } 305 306 if(ckBtcBalance && amountBigInt > ckBtcBalance) { 307 console.log("Amount exceeds balance"); 308 } 309 310 try { 311 if(!destinationPrincipal || !amountBigInt) return; 312 const res = ledgerActor.transfer({ 313 to: { 314 owner: destinationPrincipal, 315 subaccount: [], 316 }, 317 amount: amountBigInt, 318 }) 319 if(res) { 320 console.log("Transfer Successful: ", res); 321 } else { 322 console.log("An error occured") 323 } 324 } catch(error) { 325 console.log("There was an issue with completing the transfer: ", error); 326 } 327 } 328 329 const handleSendButton = async () => { 330 console.log("Selected account currency: ",selectedAccount?.currency); 331 console.log("......Submiting to data base........"); 332 await updateAccountRecordsSenderData(); 333 console.log("Succesfully added sender data to database") 334 await updateAccountRecordsReceiverData(); 335 console.log("Successfully added receiver data to database"); 336 if(selectedAccount?.currency === "CKBTC") { 337 handleTransferCkBtc(); 338 } 339 handleClose(); 340 }; 341 342 return ( 343 <Dialog.Root open={open} onClose={handleClose} ref={dialogRef} style={{ alignItems: 'center', justifyContent: 'center' }}> 344 <Dialog.Overlay className={styles.DialogOverlay} /> 345 <Dialog.Content className={`${styles.DialogContent} ${styles.centeredContent}`}> 346 <Dialog.Close asChild> 347 <button className={styles.DialogCloseButton} onClick={handleClose}> 348 <Cross2Icon /> 349 </button> 350 </Dialog.Close> 351 <Dialog.Title className={styles.DialogTitle}>Send</Dialog.Title> 352 353 <fieldset> 354 <label className={styles.SendModalLabel}>Destination</label> 355 <select 356 className={styles.SendModalInput} 357 name="destination" 358 id="destination" 359 onChange={handleDestinationChange} 360 value={selectedDestinationAccount?.accountName || ""} 361 > 362 363 <option value="" disabled>Select destination account</option> 364 {allAccountRecordsData?.filter(accountRecord => accountRecord.accountName !== selectedAccount.accountName).map((accountRecord)=> ( 365 <option key={accountRecord.accountName} value={accountRecord.accountName}>{accountRecord.accountName} {accountRecord.currency}</option> 366 ))} 367 </select> 368 <label className={styles.SendModalLabel}>Amount</label> 369 <input 370 className={styles.SendModalInput} 371 name="amount" 372 type="number" 373 id="amount" 374 onChange={handleAmountChange} 375 value={amount || inputAmount} 376 /> 377 </fieldset> 378 379 <div className={styles.AddressContainer}> 380 <span className={styles.AddressLabel}>Source:</span> 381 <span className={styles.AddressValue}>{principal}</span> 382 </div> 383 {/* <div className={styles.AddressContainer}> 384 <span className={styles.AddressLabel}>Dollar Worth:</span> 385 <span className={styles.AddressValue}>{usdAmount}</span> 386 </div> */} 387 <div className={styles.AddressContainer}> 388 <span className={styles.AddressLabel}>Balance:</span> 389 <span className={styles.AddressValue}>{latestBalance} {selectedAccount?.accountName === "CKBTC" ? ckBtcBalance : selectedAccount?.currency}</span> 390 </div> 391 {/* <div className={styles.AddressContainer}> 392 <span className={styles.AddressLabel}>Balance($):</span> 393 <span className={styles.AddressValue}>{latestBalance !== null ? `${usdBalance} USD` : ''}</span> 394 </div> */} 395 <div className={styles.ButtonContainer}> 396 <Button 397 className={`${styles.Button} ${styles.SendModalCancelButton}`} 398 onClick={handleClose} 399 > 400 Cancel 401 </Button> 402 <Button 403 className={`${styles.Button} ${styles.SendModalNextButton}`} 404 onClick={handleSendButton} 405 > 406 Send 407 </Button> 408 </div> 409 </Dialog.Content> 410 </Dialog.Root> 411 ); 412 }; 413 414 export default SendModal;