diff --git a/src/pages/wrap/components/WrapForm.tsx b/src/pages/wrap/components/WrapForm.tsx
index 9e5d5797..163c8400 100644
--- a/src/pages/wrap/components/WrapForm.tsx
+++ b/src/pages/wrap/components/WrapForm.tsx
@@ -1,391 +1,650 @@
-import { faRightLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
-import Select, { components } from 'react-select'
+import { useState, useMemo } from 'react'
import BigNumber from 'bignumber.js'
-import { useFormik } from 'formik'
-import { useEffect } from 'react'
-import FeeGrant from 'components/FeeGrant/FeeGrant'
-import PercentagePicker from 'components/PercentagePicker'
-import { WrappingMode, isWrappingMode } from 'types/WrappingMode'
-import { Token, tokens } from 'utils/config'
-import { useSecretNetworkClientStore } from 'store/secretNetworkClient'
-import { wrapSchema } from 'pages/wrap/wrapSchema'
-import Tooltip from '@mui/material/Tooltip'
+import { tokens } from 'utils/config'
import { WrapService } from 'services/wrap.service'
-import BalanceUI from 'components/BalanceUI'
-import toast from 'react-hot-toast'
-import { useSearchParams } from 'react-router-dom'
-import { Nullable } from 'types/Nullable'
+import { NotificationService } from 'services/notification.service'
+import { useSecretNetworkClientStore } from 'store/secretNetworkClient'
import { useUserPreferencesStore } from 'store/UserPreferences'
-import { debugModeOverride } from 'utils/commons'
+import BalanceUI from 'components/BalanceUI'
+import FeeGrant from 'components/FeeGrant/FeeGrant'
import { GetBalanceError } from 'types/GetBalanceError'
-import { NotificationService } from 'services/notification.service'
+import './wrap.scss'
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
+import { faArrowLeft, faArrowRight, faQuestion } from '@fortawesome/free-solid-svg-icons'
+import { faCircle } from '@fortawesome/free-regular-svg-icons'
+import { Tooltip } from '@mui/material'
+
+export default function WrapAllTokens() {
+ const {
+ secretNetworkClient,
+ feeGrantStatus,
+ isConnected,
+ getBalance,
+ balanceMapping,
+ ibcBalanceMapping,
+ setBalanceMapping
+ } = useSecretNetworkClientStore()
-export default function WrapForm() {
const { debugMode } = useUserPreferencesStore()
- const { secretNetworkClient, feeGrantStatus, isConnected, getBalance } = useSecretNetworkClientStore()
- const scrtBalance = getBalance(
- tokens.find((token) => token.name === 'SCRT'),
- false
- )
- const { theme } = useUserPreferencesStore()
-
- const formik = useFormik
({
- initialValues: {
- amount: '',
- token: tokens.find((token: Token) => token.name === 'SCRT'),
- wrappingMode: 'wrap'
- },
- validationSchema: wrapSchema,
- validateOnBlur: false,
- validateOnChange: true,
- onSubmit: async (values) => {
- const toastId = NotificationService.notify(
- `Waiting to ${formik.values.wrappingMode === 'wrap' ? 'wrap' : 'unwrap'} ${formik.values.amount} ${
- formik.values.token.name
- }...`,
- 'loading'
- )
-
- try {
- const res = WrapService.performWrapping({
- ...values,
- secretNetworkClient,
- feeGrantStatus
- })
- res
- .then(() => {
- NotificationService.notify(
- `${formik.values.wrappingMode === 'wrap' ? 'Wrapping' : 'Unwrapping'} of ${formik.values.amount} ${
- formik.values.token.name
- } successful`,
- 'success',
- toastId
- )
- })
- .catch((error) => {
- console.error(error)
- NotificationService.notify(
- `${formik.values.wrappingMode === 'wrap' ? 'Wrapping' : 'Unwrapping'} of ${formik.values.amount} ${
- formik.values.token.name
- } unsuccessful: ${error}`,
- 'error',
- toastId
- )
- })
- } catch (error: any) {
- console.error(error)
- NotificationService.notify(
- `${formik.values.wrappingMode === 'wrap' ? 'Wrapping' : 'Unwrapping'} of ${formik.values.amount} ${
- formik.values.token.name
- } unsuccessful: ${error}`,
- 'error',
- toastId
- )
- }
+ // Tracks user's choice (wrap/unwrap) and amount for each token.
+ const [batchOperations, setBatchOperations] = useState<{
+ [tokenName: string]: {
+ direction: 'wrap' | 'unwrap'
+ amount: string
}
- })
+ }>({})
+
+ // Toggles for filtering tokens.
+ const [hideZeroBalance, setHideZeroBalance] = useState(true)
+ const [hideNoViewingKey, setHideNoViewingKey] = useState(true)
+
+ // Toggle to show an amount input field (for "pro" users).
+ const [showAmountInput, setShowAmountInput] = useState(false)
- function handleTokenSelect(token: Token) {
- formik.setFieldValue('token', token)
- formik.setFieldValue('amount', '')
- formik.setFieldTouched('amount', false)
+ // New state to control whether the full list is shown or just the first 5 tokens.
+ const [expanded, setExpanded] = useState(false)
+
+ // Fetch a balance safely (returns a BigNumber, 'viewingKeyError', 'GenericFetchError', or null).
+ function getBalanceSpecial(token: (typeof tokens)[number], isSecretToken: boolean) {
+ const result = getBalance(token, isSecretToken)
+ if (result === 'viewingKeyError') return 'viewingKeyError' as GetBalanceError
+ if (result === 'GenericFetchError') return 'GenericFetchError' as GetBalanceError
+ if (result instanceof BigNumber) return result
+ return null
}
- // URL params
- const [searchParams, setSearchParams] = useSearchParams()
- const wrappingModeUrlParam = searchParams.get('mode')
- const tokenUrlParam = searchParams.get('token')
+ // Check if a token has a valid viewing key.
+ function hasViewingKey(token: (typeof tokens)[number]): boolean {
+ const secretBalance = getBalanceSpecial(token, true)
+ if (secretBalance === 'viewingKeyError') return false
+ if (secretBalance === 'GenericFetchError') return false
+ if (secretBalance === null) return false
+ return true
+ }
- useEffect(() => {
- // sets token by searchParam
- let foundToken: Nullable = null
- if (tokenUrlParam) {
- foundToken = tokens.find((token: Token) => token.name.toLowerCase() === tokenUrlParam)
- }
- if (foundToken) {
- formik.setFieldValue('token', foundToken)
- }
+ // Filter tokens based on toggles (hide zero balance, hide no viewing key).
+ const filteredTokens = useMemo(() => {
+ return tokens.filter((token) => {
+ if (hideNoViewingKey && !hasViewingKey(token)) {
+ return false
+ }
+ if (hideZeroBalance) {
+ const unwrapped = getBalanceSpecial(token, false)
+ const secret = getBalanceSpecial(token, true)
+
+ const unwrappedBN = unwrapped instanceof BigNumber ? unwrapped : new BigNumber(0)
+ const secretBN = secret instanceof BigNumber ? secret : new BigNumber(0)
+ const total = unwrappedBN.plus(secretBN)
+
+ if (total.isZero()) {
+ return false
+ }
+ }
+ return true
+ })
+ }, [hideZeroBalance, hideNoViewingKey, balanceMapping, ibcBalanceMapping])
+
+ const tokensToDisplay = useMemo(() => {
+ // Sort filtered tokens by unwrapped (public) balance descending.
+ const sortedTokens = [...filteredTokens].sort((a, b) => {
+ const balanceAResult = getBalanceSpecial(a, false)
+ const balanceBResult = getBalanceSpecial(b, false)
+ const balanceA = balanceAResult instanceof BigNumber ? balanceAResult : new BigNumber(0)
+ const balanceB = balanceBResult instanceof BigNumber ? balanceBResult : new BigNumber(0)
+ return balanceB.minus(balanceA).toNumber()
+ })
+
+ // If expanded, display all tokens.
+ if (expanded) return sortedTokens
+
+ // Otherwise, take the top 5 tokens.
+ const topTokens = sortedTokens.slice(0, 5)
+
+ // Also include any tokens that have been selected via batchOperations.
+ const selectedTokens = sortedTokens.filter((token) => batchOperations[token.name])
+ const unionTokens = [...topTokens]
+ selectedTokens.forEach((token) => {
+ if (!unionTokens.find((t) => t.name === token.name)) {
+ unionTokens.push(token)
+ }
+ })
+ return unionTokens
+ }, [expanded, filteredTokens, batchOperations])
+
+ // Update the amount value for a given token.
+ const updateAmount = (tokenName: string, amount: string) => {
+ setBatchOperations((prev) => ({
+ ...prev,
+ [tokenName]: {
+ ...prev[tokenName],
+ amount
+ }
+ }))
+ }
+
+ // When clicking a balance, set both the amount and the mode (wrap or unwrap).
+ const handleBalanceClick = (tokenName: string, balanceStr: string, mode: 'wrap' | 'unwrap') => {
+ setBatchOperations((prev) => ({
+ ...prev,
+ [tokenName]: {
+ amount: balanceStr,
+ direction: mode
+ }
+ }))
+ }
+
+ // Toggle the operation direction between 'wrap', 'unwrap', and 'nothing'.
+ const toggleDirection = (tokenName: string) => {
+ setBatchOperations((prev: any) => {
+ const token = tokens.find((t) => t.name === tokenName)
+ if (!token) return prev
+
+ const unwrappedBalance = getBalanceSpecial(token, false)
+ const wrappedBalance = getBalanceSpecial(token, true)
+
+ // If token is not in batchOperations yet, add it with default wrap direction
+ if (!prev[tokenName]) {
+ // Default to wrap if there's unwrapped balance, otherwise unwrap
+ const defaultDirection = unwrappedBalance instanceof BigNumber && !unwrappedBalance.isZero() ? 'wrap' : 'unwrap'
+
+ // Get the appropriate balance amount based on the chosen direction
+ const balanceToUse = defaultDirection === 'wrap' ? unwrappedBalance : wrappedBalance
+
+ // Calculate the human-readable amount if we have a valid balance
+ let amountToUse = ''
+ if (balanceToUse instanceof BigNumber && !balanceToUse.isZero()) {
+ const decimals = token.decimals || 6
+ amountToUse = balanceToUse.dividedBy(new BigNumber(10).pow(decimals)).toString()
+ }
+
+ return {
+ ...prev,
+ [tokenName]: {
+ direction: defaultDirection,
+ amount: amountToUse
+ }
+ }
+ }
+
+ // Define the next direction in the cycle: wrap -> unwrap -> nothing -> wrap
+ let newDirection
+ if (prev[tokenName].direction === 'wrap') {
+ newDirection = 'unwrap'
+ } else if (prev[tokenName].direction === 'unwrap') {
+ // When going to "nothing", remove the token from batch operations entirely
+ const newOps = { ...prev }
+ delete newOps[tokenName]
+ return newOps
+ } else {
+ newDirection = 'wrap'
+ }
+
+ // Get the appropriate balance amount based on the chosen direction
+ let newAmount = newDirection === 'wrap' ? unwrappedBalance : wrappedBalance
+
+ let amountToUse = ''
+ if (newAmount instanceof BigNumber) {
+ const decimals = token.decimals || 6
+ amountToUse = newAmount.dividedBy(new BigNumber(10).pow(decimals)).toString()
+ }
- // sets wrappingMode by searchParam
- if (wrappingModeUrlParam && isWrappingMode(wrappingModeUrlParam)) {
- formik.setFieldValue('wrappingMode', wrappingModeUrlParam)
- formik.setFieldTouched('wrappingMode')
+ return {
+ ...prev,
+ [tokenName]: {
+ ...prev[tokenName],
+ direction: newDirection,
+ amount: amountToUse
+ }
+ }
+ })
+ }
+
+ // Handle checkbox change for a token
+ const handleChange = (tokenName: string, checked: boolean) => {
+ if (checked) {
+ // When checking the box, add the token to batch operations with default direction
+ // and a meaningful amount based on available balance
+ const token = tokens.find((t) => t.name === tokenName)
+ if (!token) return
+ // Determine which balance to use based on the direction we'll choose
+ const unwrappedBalance = getBalanceSpecial(token, false)
+ const wrappedBalance = getBalanceSpecial(token, true)
+
+ // Default to wrap if there's unwrapped balance, otherwise unwrap
+ const defaultDirection = unwrappedBalance instanceof BigNumber && !unwrappedBalance.isZero() ? 'wrap' : 'unwrap'
+
+ // Get the appropriate balance amount based on the chosen direction
+ const balanceToUse = defaultDirection === 'wrap' ? unwrappedBalance : wrappedBalance
+
+ // Calculate the human-readable amount if we have a valid balance
+ let amountToUse = ''
+ if (balanceToUse instanceof BigNumber && !balanceToUse.isZero()) {
+ const decimals = token.decimals || 6
+ amountToUse = balanceToUse.dividedBy(new BigNumber(10).pow(decimals)).toString()
+ }
+
+ setBatchOperations((prev) => ({
+ ...prev,
+ [tokenName]: {
+ direction: defaultDirection,
+ amount: amountToUse
+ }
+ }))
+ } else {
+ // When unchecking, remove the token from batch operations
+ setBatchOperations((prev) => {
+ const newOps = { ...prev }
+ delete newOps[tokenName]
+ return newOps
+ })
}
- }, [])
+ }
+
+ // Perform the entire batch transaction.
+ const handlePerformBatch = async () => {
+ if (!isConnected) return
+
+ const items = []
+ for (const tokenName in batchOperations) {
+ const op = batchOperations[tokenName]
+ // Ensure a valid amount is specified.
+ if (op && op.amount && parseFloat(op.amount) > 0) {
+ // Find the corresponding token from the full tokens array.
+ const token = tokens.find((t) => t.name === tokenName)
+ if (token.name === 'SCRT' && op.direction === 'wrap') {
+ const rawBalance = getBalanceSpecial(token, false)
+ if (rawBalance instanceof BigNumber) {
+ const amount = parseFloat(rawBalance.dividedBy(`1e${token.decimals}`).toString())
+ const requestedAmount = parseFloat(op.amount)
+ const minReserve = 0.5
- useEffect(() => {
- var params = {}
- if (formik.values.wrappingMode) {
- params = { ...params, mode: formik.values.wrappingMode.toLowerCase() }
+ // Calculate available amount while maintaining minimum reserve
+ const safeAmount = Math.max(0, amount - minReserve)
+ const adjustedAmount = Math.min(requestedAmount, safeAmount)
+
+ if (adjustedAmount > 0) {
+ items.push({
+ token,
+ amount: adjustedAmount.toString(),
+ wrappingMode: op.direction
+ })
+ }
+ }
+ } else if (token) {
+ items.push({
+ token,
+ amount: op.amount,
+ wrappingMode: op.direction // 'wrap' or 'unwrap'
+ })
+ }
+ }
}
- if (formik.values.token) {
- params = { ...params, token: formik.values.token.name.toLowerCase() }
+
+ if (!items.length) {
+ NotificationService.notify('No valid items selected for batch transaction', 'error')
+ return
}
- setSearchParams(params)
- }, [formik.values.wrappingMode, formik.values.token])
- function toggleWrappingMode() {
- if (formik.values.wrappingMode === 'wrap') {
- formik.setFieldValue('wrappingMode', 'unwrap')
- } else {
- formik.setFieldValue('wrappingMode', 'wrap')
+ const toastId = NotificationService.notify('Executing batch transaction...', 'loading')
+ try {
+ await WrapService.performBatchWrapping({
+ items,
+ secretNetworkClient,
+ feeGrantStatus
+ })
+ NotificationService.notify('Batch transaction successful!', 'success', toastId)
+ setBalanceMapping()
+ setBatchOperations({})
+ } catch (error) {
+ console.error(error)
+ NotificationService.notify(`Batch transaction failed: ${error}`, 'error', toastId)
}
}
- const secretToken: Token = tokens.find((token) => token.name === 'SCRT')
+ // "Wrap All" button: fill all unwrapped balances into batch operations for wrapping.
+ const handleWrapAll = () => {
+ const newBatchOperations = { ...batchOperations }
- useEffect(() => {
- if (Number(scrtBalance) === 0 && scrtBalance !== null) {
- formik.setFieldValue('wrappingMode', 'unwrap')
- formik.setFieldValue('token', secretToken)
- }
- }, [scrtBalance])
-
- function setAmountByPercentage(percentage: number) {
- const balance = getBalance(formik.values.token, formik.values.wrappingMode === 'unwrap')
-
- if (
- (balance !== ('viewingKeyError' as GetBalanceError) || balance !== ('GenericFetchError' as GetBalanceError)) &&
- balance !== null
- ) {
- const scaledAmount = (balance as BigNumber)
- .times(percentage / 100)
- .minus((balance as BigNumber).times(percentage / 100).gt(1) ? 1 : 0)
- .dividedBy(`1e${formik.values.token.decimals}`)
- .decimalPlaces(formik.values.token.decimals, BigNumber.ROUND_DOWN)
-
- formik.setFieldValue('amount', scaledAmount.toFixed(formik.values.token.decimals))
+ for (const token of filteredTokens) {
+ if (token.name === 'SCRT') {
+ continue
+ }
+ const rawBalance = getBalanceSpecial(token, false)
+ if (rawBalance instanceof BigNumber) {
+ const amount = rawBalance.dividedBy(`1e${token.decimals}`).toString()
+ newBatchOperations[token.name] = {
+ direction: 'wrap',
+ amount
+ }
+ }
}
- formik.setFieldTouched('amount', true)
+ setBatchOperations(newBatchOperations)
}
- // auto focus on connect
- useEffect(() => {
- if (isConnected) {
- document.getElementById('amount-top').focus()
+ // "Unwrap All" button: fill all wrapped balances into batch operations for unwrapping.
+ const handleUnwrapAll = () => {
+ const newBatchOperations = { ...batchOperations }
+
+ for (const token of filteredTokens) {
+ if (token.name === 'SCRT') {
+ continue
+ }
+ const wrappedBalance = getBalanceSpecial(token, true)
+ if (wrappedBalance instanceof BigNumber) {
+ const amount = wrappedBalance.dividedBy(`1e${token.decimals}`).toString()
+ newBatchOperations[token.name] = {
+ direction: 'unwrap',
+ amount
+ }
+ }
}
- }, [isConnected])
-
- const customTokenFilterOption = (option: any, inputValue: string) => {
- const tokenName = option.data.name.toLowerCase()
- return (
- tokenName?.toLowerCase().includes(inputValue?.toLowerCase()) ||
- ('s' + tokenName)?.toLowerCase().includes(inputValue?.toLowerCase()) ||
- ('secret' + tokenName)?.toLowerCase().includes(inputValue?.toLowerCase())
- )
+
+ setBatchOperations(newBatchOperations)
}
- const customTokenSelectStyle = {
- input: (styles: any) => ({
- ...styles,
- color: theme === 'light' ? 'black !important' : 'white !important',
- fontFamily: 'RundDisplay, sans-serif',
- fontWeight: 600,
- fontSize: '14px'
- }),
- container: (container: any) => ({
- ...container,
- width: 'auto',
- minWidth: '30%'
+ // A small summary to show how many tokens are set to wrap vs unwrap, and total amounts.
+ const summary = useMemo(() => {
+ let wrapCount = 0
+ let unwrapCount = 0
+
+ Object.values(batchOperations).forEach((op) => {
+ if (op && op.amount && parseFloat(op.amount) > 0) {
+ if (op.direction === 'wrap') {
+ wrapCount++
+ } else if (op.direction === 'unwrap') {
+ unwrapCount++
+ }
+ }
})
- }
- const CustomControl = ({ children, ...props }: any) => {
- const menuIsOpen = props.selectProps.menuIsOpen
- return (
-
-
- {menuIsOpen && }
- {children}
-
-
- )
- }
+ return {
+ wrapCount,
+ unwrapCount
+ }
+ }, [batchOperations])
- interface IFormValues {
- amount: string
- token: Token
- wrappingMode: WrappingMode
- }
+ const isLoading = balanceMapping == null && ibcBalanceMapping == null
return (
-