diff --git a/README.md b/README.md index 227103e..a4d2615 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ # Boring Vault UI A reusable package to quickly integrate boring vaults onto a UI. +Full documentation for using this package can be found at https://www.notion.so/7seascapital/Boring-Vault-UI-SDK-545da170a9f349259176fa96eb000423 # Local Pkg Development Setup 1. Clone the repository diff --git a/package-lock.json b/package-lock.json index df411bb..a30a5e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "boring-vault-ui", - "version": "1.4.2", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "boring-vault-ui", - "version": "1.4,2", + "version": "1.5.0", "license": "MIT", "dependencies": { "@chakra-ui/react": "^2.8.2", diff --git a/package.json b/package.json index 96ccfa8..1c8c0aa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "boring-vault-ui", - "version": "1.4.2", + "version": "1.5.0", "description": "A reusable package to quickly integrate boring vaults onto a UI.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/abis/v1/BoringWithdrawQueueContractABI.tsx b/src/abis/v1/BoringWithdrawQueueContractABI.tsx new file mode 100644 index 0000000..df43e40 --- /dev/null +++ b/src/abis/v1/BoringWithdrawQueueContractABI.tsx @@ -0,0 +1,233 @@ +export default [ + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "AtomicQueue__RequestDeadlineExceeded", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "AtomicQueue__UserNotInSolve", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "AtomicQueue__UserRepeated", + type: "error", + }, + { + inputs: [{ internalType: "address", name: "user", type: "address" }], + name: "AtomicQueue__ZeroOfferAmount", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "offerToken", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "wantToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "offerAmountSpent", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "wantAmountReceived", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "timestamp", + type: "uint256", + }, + ], + name: "AtomicRequestFulfilled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "user", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "offerToken", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "wantToken", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "deadline", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "minPrice", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "timestamp", + type: "uint256", + }, + ], + name: "AtomicRequestUpdated", + type: "event", + }, + { + inputs: [ + { internalType: "address", name: "user", type: "address" }, + { internalType: "contract ERC20", name: "offer", type: "address" }, + { internalType: "contract ERC20", name: "want", type: "address" }, + ], + name: "getUserAtomicRequest", + outputs: [ + { + components: [ + { internalType: "uint64", name: "deadline", type: "uint64" }, + { internalType: "uint88", name: "atomicPrice", type: "uint88" }, + { internalType: "uint96", name: "offerAmount", type: "uint96" }, + { internalType: "bool", name: "inSolve", type: "bool" }, + ], + internalType: "struct AtomicQueue.AtomicRequest", + name: "", + type: "tuple", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "contract ERC20", name: "offer", type: "address" }, + { internalType: "address", name: "user", type: "address" }, + { + components: [ + { internalType: "uint64", name: "deadline", type: "uint64" }, + { internalType: "uint88", name: "atomicPrice", type: "uint88" }, + { internalType: "uint96", name: "offerAmount", type: "uint96" }, + { internalType: "bool", name: "inSolve", type: "bool" }, + ], + internalType: "struct AtomicQueue.AtomicRequest", + name: "userRequest", + type: "tuple", + }, + ], + name: "isAtomicRequestValid", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "contract ERC20", name: "offer", type: "address" }, + { internalType: "contract ERC20", name: "want", type: "address" }, + { internalType: "address[]", name: "users", type: "address[]" }, + { internalType: "bytes", name: "runData", type: "bytes" }, + { internalType: "address", name: "solver", type: "address" }, + ], + name: "solve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "contract ERC20", name: "offer", type: "address" }, + { internalType: "contract ERC20", name: "want", type: "address" }, + { + components: [ + { internalType: "uint64", name: "deadline", type: "uint64" }, + { internalType: "uint88", name: "atomicPrice", type: "uint88" }, + { internalType: "uint96", name: "offerAmount", type: "uint96" }, + { internalType: "bool", name: "inSolve", type: "bool" }, + ], + internalType: "struct AtomicQueue.AtomicRequest", + name: "userRequest", + type: "tuple", + }, + ], + name: "updateAtomicRequest", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "contract ERC20", name: "", type: "address" }, + { internalType: "contract ERC20", name: "", type: "address" }, + ], + name: "userAtomicRequest", + outputs: [ + { internalType: "uint64", name: "deadline", type: "uint64" }, + { internalType: "uint88", name: "atomicPrice", type: "uint88" }, + { internalType: "uint96", name: "offerAmount", type: "uint96" }, + { internalType: "bool", name: "inSolve", type: "bool" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "contract ERC20", name: "offer", type: "address" }, + { internalType: "contract ERC20", name: "want", type: "address" }, + { internalType: "address[]", name: "users", type: "address[]" }, + ], + name: "viewSolveMetaData", + outputs: [ + { + components: [ + { internalType: "address", name: "user", type: "address" }, + { internalType: "uint8", name: "flags", type: "uint8" }, + { internalType: "uint256", name: "assetsToOffer", type: "uint256" }, + { internalType: "uint256", name: "assetsForWant", type: "uint256" }, + ], + internalType: "struct AtomicQueue.SolveMetaData[]", + name: "metaData", + type: "tuple[]", + }, + { internalType: "uint256", name: "totalAssetsForWant", type: "uint256" }, + { internalType: "uint256", name: "totalAssetsToOffer", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, +]; diff --git a/src/components/v1/PendingWithdrawQueueStatuses.tsx b/src/components/v1/PendingWithdrawQueueStatuses.tsx new file mode 100644 index 0000000..b64cde9 --- /dev/null +++ b/src/components/v1/PendingWithdrawQueueStatuses.tsx @@ -0,0 +1,79 @@ +import React, { useEffect, useState } from "react"; +import { Box, HStack, Text, VStack } from "@chakra-ui/react"; +import { useBoringVaultV1 } from "../../contexts/v1/BoringVaultContextV1"; +import { useEthersSigner } from "../../hooks/ethers"; +import { WithdrawQueueStatus } from "../../types"; +import WithdrawQueueCancelButton from "./WithdrawQueueCancelButton"; + +interface PendingWithdrawQueueStatusesProps { + title?: string; // Optional title +} + +// TODO Abstract away style into props above same as DepositButton +const PendingWithdrawQueueStatuses: React.FC< + PendingWithdrawQueueStatusesProps +> = ({ title, ...pendingWithdrawQueueProps }) => { + const { isConnected, userAddress, ethersProvider, withdrawQueueStatuses } = + useBoringVaultV1(); + const [statuses, setStatuses] = useState([]); // State to store fetched statuses + const signer = useEthersSigner(); + + useEffect(() => { + const fetchStatuses = async () => { + const fetchedStatuses: WithdrawQueueStatus[] = + await withdrawQueueStatuses(signer!); + setStatuses(fetchedStatuses); + }; + + fetchStatuses(); + console.log("withdrawQueueStatuses", withdrawQueueStatuses); + }, [withdrawQueueStatuses, signer]); + + return ( + + {title && ( + + {title} + + )} + + {statuses.map((withdrawStatus: WithdrawQueueStatus, index) => { + return ( + + + + + Shares Withdrawing:{" "} + {withdrawStatus.sharesWithdrawing} + + + Token Out:{" "} + {withdrawStatus.tokenOut.displayName} + + + Expiration (unix seconds):{" "} + {withdrawStatus.deadlineUnixSeconds} + + + Target Token Price:{" "} + {withdrawStatus.minSharePrice} + + + + + + + + ); + })} + + + ); +}; + +export default PendingWithdrawQueueStatuses; diff --git a/src/components/v1/WithdrawQueueButton.tsx b/src/components/v1/WithdrawQueueButton.tsx new file mode 100644 index 0000000..ed3cc10 --- /dev/null +++ b/src/components/v1/WithdrawQueueButton.tsx @@ -0,0 +1,270 @@ +// src/components/v1/WithdrawQueueButton.tsx + +import React, { useEffect } from "react"; +import { + Button, + Modal, + ModalOverlay, + ModalContent, + ModalBody, + ModalCloseButton, + useDisclosure, + ButtonProps, + ModalProps, + ModalOverlayProps, + ModalContentProps, + ModalBodyProps, + ModalCloseButtonProps, + Text, + HStack, + VStack, + Box, + Image, + Select, + InputGroup, + Input, + InputRightElement, + FormControl, + Flex, + FormHelperText, + FormLabel, + InputProps, + ButtonGroup, + ModalHeader, + ModalFooter, + Avatar, + useToast, +} from "@chakra-ui/react"; +import { useBoringVaultV1 } from "../../contexts/v1/BoringVaultContextV1"; +import { Token } from "../../types"; +import { Contract, formatUnits } from "ethers"; +import { erc20Abi } from "viem"; +import { useEthersSigner } from "../../hooks/ethers"; + +interface WithdrawQueueButtonProps { + buttonText: string; + popupText: string; + title?: string; // Optional title + bottomText?: string; // Optional bottom text (e.g. disclaimer, etc.) + buttonProps?: ButtonProps; + modalProps?: ModalProps; + modalOverlayProps?: ModalOverlayProps; + modalContentProps?: ModalContentProps; + modalBodyProps?: ModalBodyProps; + modalCloseButtonProps?: ModalCloseButtonProps; + inputProps?: any; +} + +const WithdrawQueueButton: React.FC = ({ + buttonText, + buttonProps, + modalProps, + modalOverlayProps, + modalContentProps, + modalBodyProps, + modalCloseButtonProps, + inputProps, + title, + bottomText, + ...withdrawButtonProps +}) => { + const { isOpen, onOpen, onClose } = useDisclosure(); + const { + withdrawTokens, + isConnected, + userAddress, + ethersProvider, + withdrawStatus, + queueWithdraw, + fetchUserShares, + } = useBoringVaultV1(); + + const [selectedToken, setSelectedToken] = React.useState( + withdrawTokens[0] + ); + const [balance, setBalance] = React.useState(0.0); + const [withdrawAmount, setWithdrawAmount] = React.useState(""); + const [discountPercent, setDiscountPercent] = React.useState(""); + const [daysValid, setDaysValid] = React.useState(""); + const signer = useEthersSigner(); + + useEffect(() => { + async function fetchBalance() { + if (!userAddress || !ethersProvider) return; + + try { + const shareBalance = await fetchUserShares(); + setBalance(shareBalance); + } catch (error) { + console.error("Failed to fetch share balance:", error); + setBalance(0); // Optionally reset balance on error + } + } + + fetchBalance(); + }, [userAddress, ethersProvider]); + + const handleSelectChange = (event: React.ChangeEvent) => { + const newTokenAddress = event.target.value; + console.log("New token address:", newTokenAddress); + console.log("Withdraw tokens:", withdrawTokens); + const newSelectedToken = withdrawTokens.find( + (token) => token.address === newTokenAddress + ); + setSelectedToken(newSelectedToken || withdrawTokens[0]); + }; + + // TODO: Allow people to pass in a toast to allow for custom toast branding + const toast = useToast(); + useEffect(() => { + if (withdrawStatus.loading) { + toast({ + title: "Processing withdraw...", + status: "info", + duration: 5000, + isClosable: true, + }); + } else if (withdrawStatus.success) { + toast({ + title: "Intent successful", + // Add link to etherscan + description: `Transaction hash: ${withdrawStatus.tx_hash}`, + status: "success", + duration: 5000, + isClosable: true, + }); + } else if (withdrawStatus.error) { + toast({ + title: "Failed to initiate withdraw", + description: withdrawStatus.error, + status: "error", + duration: 5000, + isClosable: true, + }); + } + }, [withdrawStatus, toast]); + + return ( + <> + + + + + {title && {title}} + + + + + + + Asset Out:{" "} + + + + + + + Shares to Redeem:{" "} + + {/* TODO: Sterilize input to only allow positive numbers */} + setWithdrawAmount(e.target.value)} + {...inputProps} + /> + + + Share Balance: {balance} + + + + + + Discount Percent (if share value is 1, a discount of 1% + means you'll accept a share price of 0.99):{" "} + + {/* TODO: Sterilize input to only allow positive numbers */} + setDiscountPercent(e.target.value)} + {...inputProps} + /> + + + + + + Days Valid (until order expires if unfulfilled):{" "} + + {/* TODO: Sterilize input to only allow positive numbers */} + setDaysValid(e.target.value)} + {...inputProps} + /> + + + + + + {/* Example static value, replace with actual conversion */} + + + + {bottomText && ( + + {bottomText} + + )} + + + + ); +}; + +export default WithdrawQueueButton; diff --git a/src/components/v1/WithdrawQueueCancelButton.tsx b/src/components/v1/WithdrawQueueCancelButton.tsx new file mode 100644 index 0000000..a315c9a --- /dev/null +++ b/src/components/v1/WithdrawQueueCancelButton.tsx @@ -0,0 +1,32 @@ +// src/components/v1/WithdrawQueueCancelButton.tsx + +import React, { useEffect, useState } from "react"; +import { Box, Text, VStack, Button } from "@chakra-ui/react"; +import { useBoringVaultV1 } from "../../contexts/v1/BoringVaultContextV1"; +import { Token } from "../../types"; +import { useEthersSigner } from "../../hooks/ethers"; + +interface WithdrawQueueCancelButtonProps { + token: Token; +} + +const WithdrawQueueCancelButton: React.FC = ({ + token, +}) => { + const { withdrawQueueCancel } = useBoringVaultV1(); + const signer = useEthersSigner(); + + return ( + + ); +}; + +export default WithdrawQueueCancelButton; diff --git a/src/contexts/v1/BoringVaultContextV1.tsx b/src/contexts/v1/BoringVaultContextV1.tsx index 639219b..65ec09b 100644 --- a/src/contexts/v1/BoringVaultContextV1.tsx +++ b/src/contexts/v1/BoringVaultContextV1.tsx @@ -12,12 +12,14 @@ import { DepositStatus, WithdrawStatus, DelayWithdrawStatus, + WithdrawQueueStatus, Token, } from "../../types"; import BoringVaultABI from "../../abis/v1/BoringVaultABI"; import BoringTellerABI from "../../abis/v1/BoringTellerABI"; import BoringAccountantABI from "../../abis/v1/BoringAccountantABI"; import BoringLensABI from "../../abis/v1/BoringLensABI"; +import BoringWithdrawQueueContractABI from "../../abis/v1/BoringWithdrawQueueContractABI"; import { Provider, Contract, @@ -28,12 +30,16 @@ import { erc20Abi } from "viem"; import BigNumber from "bignumber.js"; import BoringDelayWithdrawContractABI from "../../abis/v1/BoringDelayWithdrawContractABI"; +const SEVEN_SEAS_BASE_API_URL = "https://api.sevenseas.capital"; + interface BoringVaultV1ContextProps { + chain: string; vaultEthersContract: Contract | null; tellerEthersContract: Contract | null; accountantEthersContract: Contract | null; lensEthersContract: Contract | null; delayWithdrawEthersContract: Contract | null; + withdrawQueueEthersContract: Contract | null; depositTokens: Token[]; withdrawTokens: Token[]; isConnected: boolean; @@ -53,6 +59,7 @@ interface BoringVaultV1ContextProps { amount: string, token: Token ) => Promise; + /* Delay Withdraws */ delayWithdraw: ( signer: JsonRpcSigner, shareAmount: string, @@ -71,6 +78,22 @@ interface BoringVaultV1ContextProps { signer: JsonRpcSigner, tokenOut: Token ) => Promise; + /* withdrawQueue */ + queueWithdraw: ( + signer: JsonRpcSigner, + amount: string, + token: Token, + discountPercent: string, + daysValid: string + ) => Promise; + withdrawQueueCancel: ( + signer: JsonRpcSigner, + token: Token + ) => Promise; + withdrawQueueStatuses: ( + Signer: JsonRpcSigner + ) => Promise; + /* Statuses */ depositStatus: DepositStatus; withdrawStatus: WithdrawStatus; isBoringV1ContextReady: boolean; @@ -82,11 +105,13 @@ const BoringVaultV1Context = createContext( ); export const BoringVaultV1Provider: React.FC<{ + chain: string; vaultContract: string; tellerContract: string; accountantContract: string; lensContract: string; delayWithdrawContract?: string; + withdrawQueueContract?: string; depositTokens: Token[]; withdrawTokens: Token[]; ethersProvider: Provider; @@ -95,6 +120,7 @@ export const BoringVaultV1Provider: React.FC<{ children: ReactNode; }> = ({ children, + chain, depositTokens, withdrawTokens, vaultContract, @@ -102,13 +128,13 @@ export const BoringVaultV1Provider: React.FC<{ accountantContract, lensContract, delayWithdrawContract, + withdrawQueueContract, ethersProvider, vaultDecimals, baseAsset, }) => { const { address } = useAccount(); const isConnected = !!address; - const [vaultEthersContract, setVaultEthersContract] = useState(null); const [tellerEthersContract, setTellerContract] = useState( @@ -121,6 +147,8 @@ export const BoringVaultV1Provider: React.FC<{ ); const [delayWithdrawEthersContract, setDelayWithdrawEthersContract] = useState(null); + const [withdrawQueueEthersContract, setWithdrawQueueEthersContract] = + useState(null); const [baseToken, setBaseToken] = useState(null); @@ -128,6 +156,7 @@ export const BoringVaultV1Provider: React.FC<{ useState(depositTokens); const [vaultWithdrawTokens, setVaultWithdrawTokens] = useState(withdrawTokens); + const [userAddress, setUserAddress] = useState(null); const [decimals, setDecimals] = useState(null); const [isBoringV1ContextReady, setIsBoringV1ContextReady] = @@ -143,6 +172,7 @@ export const BoringVaultV1Provider: React.FC<{ useEffect(() => { if ( + chain && vaultContract && tellerContract && accountantContract && @@ -183,6 +213,15 @@ export const BoringVaultV1Provider: React.FC<{ setDelayWithdrawEthersContract(delayWithdrawEthersContract); } + if (withdrawQueueContract) { + const withdrawQueueEthersContract = new Contract( + withdrawQueueContract, + BoringWithdrawQueueContractABI, + ethersProvider + ); + setWithdrawQueueEthersContract(withdrawQueueEthersContract); + } + setVaultEthersContract(vaultEthersContract); setTellerContract(tellerEthersContract); setAccountantEthersContract(accountantEthersContract); @@ -194,6 +233,7 @@ export const BoringVaultV1Provider: React.FC<{ } else { console.warn("Boring vault contracts not initialized"); console.warn("Missing: ", { + chain, vaultContract, tellerContract, accountantContract, @@ -206,6 +246,7 @@ export const BoringVaultV1Provider: React.FC<{ }); } }, [ + chain, vaultContract, tellerContract, accountantContract, @@ -509,6 +550,8 @@ export const BoringVaultV1Provider: React.FC<{ ] ); + /* Delay Withdraws */ + const delayWithdraw = useCallback( async ( signer: JsonRpcSigner, @@ -807,7 +850,6 @@ export const BoringVaultV1Provider: React.FC<{ success: true, tx_hash: cancelReceipt.hash, }); - } catch (error: any) { console.error("Error cancelling withdraw", error); setWithdrawStatus({ @@ -819,9 +861,15 @@ export const BoringVaultV1Provider: React.FC<{ return withdrawStatus; } return withdrawStatus; - - }, [delayWithdrawEthersContract, userAddress, decimals, ethersProvider, isBoringV1ContextReady]); - + }, + [ + delayWithdrawEthersContract, + userAddress, + decimals, + ethersProvider, + isBoringV1ContextReady, + ] + ); const delayWithdrawComplete = useCallback( async (signer: JsonRpcSigner, tokenOut: Token) => { @@ -864,13 +912,15 @@ export const BoringVaultV1Provider: React.FC<{ loading: true, }); - const completeTx = await delayWithdrawContractWithSigner.completeWithdraw( - tokenOut.address, - userAddress - ); + const completeTx = + await delayWithdrawContractWithSigner.completeWithdraw( + tokenOut.address, + userAddress + ); // Wait for confirmation - const completeReceipt: ContractTransactionReceipt = await completeTx.wait(); + const completeReceipt: ContractTransactionReceipt = + await completeTx.wait(); console.log("Withdraw Completed in tx: ", completeReceipt); @@ -894,7 +944,6 @@ export const BoringVaultV1Provider: React.FC<{ success: true, tx_hash: completeReceipt.hash, }); - } catch (error: any) { console.error("Error completing withdraw", error); setWithdrawStatus({ @@ -906,17 +955,335 @@ export const BoringVaultV1Provider: React.FC<{ return withdrawStatus; } return withdrawStatus; - }, [delayWithdrawEthersContract, userAddress, decimals, ethersProvider, isBoringV1ContextReady]); + }, + [ + delayWithdrawEthersContract, + userAddress, + decimals, + ethersProvider, + isBoringV1ContextReady, + ] + ); + + /* withdrawQueue */ + const queueWithdraw = useCallback( + async ( + signer: JsonRpcSigner, + amountHumanReadable: string, + token: Token, + discountPercent: string, + daysValid: string + ) => { + if ( + !withdrawQueueEthersContract || + !vaultEthersContract || + !isBoringV1ContextReady || + !lensEthersContract || + !userAddress || + !decimals || + !signer + ) { + console.error("Contracts or user not ready", { + withdrawQueueEthersContract, + isBoringV1ContextReady, + userAddress, + decimals, + signer, + }); + + setWithdrawStatus({ + initiated: false, + loading: false, + success: false, + error: "Contracts or user not ready", + }); + + return withdrawStatus; + } + + console.log("Queueing withdraw ..."); + const withdrawQueueContractWithSigner = new Contract( + withdrawQueueContract!, + BoringWithdrawQueueContractABI, + signer + ); + + setWithdrawStatus({ + initiated: true, + loading: true, + }); + + try { + // Get the amount in base denomination + const bigNumAmt = new BigNumber(amountHumanReadable); + console.warn(amountHumanReadable); + console.warn("Amount to withdraw: ", bigNumAmt.toNumber()); + const amountWithdrawBaseDenom = bigNumAmt + .multipliedBy(new BigNumber(10).pow(vaultDecimals)) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + console.warn( + "Amount to withdraw: ", + amountWithdrawBaseDenom.toNumber() + ); + + // Get the current share price + const sharePrice = await lensEthersContract.exchangeRate( + accountantContract + ); + + // Discounted share price + const discountedSharePrice = new BigNumber(sharePrice) + .multipliedBy( + new BigNumber(100) + .minus(new BigNumber(discountPercent)) + .dividedBy(100) + ) + .decimalPlaces(0, BigNumber.ROUND_DOWN); + + // Get the days valid + const daysValidSeconds = new BigNumber(daysValid).multipliedBy( + new BigNumber(86400) // 1 day in seconds + ); + // Get the current unix time seconds and add the days valid + const deadline = new BigNumber( + Math.floor(Date.now() / 1000) + + Math.floor(daysValidSeconds.toNumber()) + ).decimalPlaces(0, BigNumber.ROUND_DOWN); + + const queueTx = + await withdrawQueueContractWithSigner.updateAtomicRequest( + vaultContract, + token.address, + [ + deadline.toNumber(), // Deadline + discountedSharePrice.toNumber(), // atomicPrice + amountWithdrawBaseDenom.toNumber(), // offerAmount + false, // inSolver + ] + ); + + // Wait for confirmation + const queueReceipt: ContractTransactionReceipt = await queueTx.wait(); + + console.log("Withdraw Queued in tx: ", queueReceipt); + if (!queueReceipt.hash) { + console.error("Withdraw Queue failed"); + setWithdrawStatus({ + initiated: false, + loading: false, + success: false, + error: "Withdraw Queue reverted", + }); + return withdrawStatus; + } + console.log("Withdraw Queue hash: ", queueReceipt.hash); + + // Set status + setWithdrawStatus({ + initiated: false, + loading: false, + success: true, + tx_hash: queueReceipt.hash, + }); + } catch (error: any) { + console.error("Error queueing withdraw", error); + setWithdrawStatus({ + initiated: false, + loading: false, + success: false, + error: (error as Error).message, + }); + return withdrawStatus; + } + + return withdrawStatus; + }, + [ + withdrawQueueEthersContract, + lensEthersContract, + userAddress, + decimals, + ethersProvider, + isBoringV1ContextReady, + ] + ); + + const withdrawQueueCancel = useCallback( + async (signer: JsonRpcSigner, token: Token) => { + if ( + !withdrawQueueEthersContract || + !isBoringV1ContextReady || + !userAddress || + !decimals || + !signer + ) { + console.error("Contracts or user not ready to cancel withdraw", { + withdrawQueueEthersContract, + isBoringV1ContextReady, + userAddress, + decimals, + signer, + }); + + setWithdrawStatus({ + initiated: false, + loading: false, + success: false, + error: "Contracts or user not ready", + }); + + return withdrawStatus; + } + + console.log("Cancelling withdraw queue ..."); + const withdrawQueueContractWithSigner = new Contract( + withdrawQueueContract!, + BoringWithdrawQueueContractABI, + signer + ); + + setWithdrawStatus({ + initiated: true, + loading: true, + }); + + try { + // Update request with same token, but 0 amount + const cancelTx = + await withdrawQueueContractWithSigner.updateAtomicRequest( + vaultContract, + token.address, + [ + 0, // Deadline + 0, // atomicPrice + 0, // offerAmount + false, // inSolver + ] + ); + + // Wait for confirmation + const cancelReceipt: ContractTransactionReceipt = await cancelTx.wait(); + console.log("Withdraw Cancelled in tx: ", cancelReceipt); + if (!cancelReceipt.hash) { + console.error("Withdraw Cancel failed"); + setWithdrawStatus({ + initiated: false, + loading: false, + success: false, + error: "Withdraw Cancel reverted", + }); + return withdrawStatus; + } + console.log("Withdraw Cancel hash: ", cancelReceipt.hash); + + // Set status + setWithdrawStatus({ + initiated: false, + loading: false, + success: true, + tx_hash: cancelReceipt.hash, + }); + } catch (error: any) { + console.error("Error cancelling withdraw", error); + setWithdrawStatus({ + initiated: false, + loading: false, + success: false, + error: (error as Error).message, + }); + return withdrawStatus; + } + + return withdrawStatus; + }, + [ + withdrawQueueEthersContract, + userAddress, + decimals, + ethersProvider, + isBoringV1ContextReady, + ] + ); + + const withdrawQueueStatuses = useCallback( + async (signer: JsonRpcSigner) => { + if ( + !withdrawQueueEthersContract || + !isBoringV1ContextReady || + !userAddress || + !decimals || + !signer + ) { + console.error( + "Contracts or user not ready for withdraw queue statuses...", + { + withdrawQueueEthersContract, + isBoringV1ContextReady, + userAddress, + decimals, + signer, + } + ); + return []; + } + console.log("Fetching withdraw queue statuses ..."); + + try { + const withdrawURL = `${SEVEN_SEAS_BASE_API_URL}/withdrawRequests/${chain.toLowerCase()}/${vaultContract}/${userAddress}`; + const response = await fetch(withdrawURL) + .then((response) => { + return response.json(); + }) + .catch((error) => { + console.error("Error fetching withdraw queue statuses", error); + return []; + }); + console.log("Response from Withdraw API: ", response); + // Parse on ["Response"]["open_requests"] + const openRequests = response["Response"]["open_requests"]; + + // Format the status object + return openRequests.map((request: any) => { + return { + sharesWithdrawing: Number(request["amount"]) / 10 ** vaultDecimals, + blockNumberOpened: Number(request["blockNumber"]), + deadlineUnixSeconds: Number(request["deadline"]), + errorCode: Number(request["errorCode"]), + minSharePrice: Number(request["minPrice"]) / 10 ** vaultDecimals, + timestampOpenedUnixSeconds: Number(request["timestamp"]), + transactionHashOpened: request["transactionHash"], + tokenOut: withdrawTokens.find( + (token) => + token.address.toLowerCase() === + request["wantToken"].toLowerCase() + )!, + } as WithdrawQueueStatus; + }); + } catch (error) { + console.error("Error fetching withdraw queue statuses", error); + return []; // Return an empty array in case of an error + } + }, + [ + withdrawQueueEthersContract, + userAddress, + decimals, + ethersProvider, + isBoringV1ContextReady, + ] + ); return ( { }, }} /> + + @@ -182,10 +218,12 @@ const App = () => { { address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", decimals: 6, }, + { + displayName: "USDe", + image: + "https://s2.coinmarketcap.com/static/img/coins/64x64/29470.png", + address: "0x9D39A5DE30e57443BfF2A8307A4256c8797A3497", + decimals: 18, + }, ]} baseAsset={{ displayName: "USDC", diff --git a/src/examples/v2.tsx b/src/examples/v2.tsx index 62d6aaf..888b5b9 100644 --- a/src/examples/v2.tsx +++ b/src/examples/v2.tsx @@ -221,6 +221,7 @@ const App = () => {