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: improved swap amount #1154

Merged
merged 1 commit into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
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
4 changes: 2 additions & 2 deletions apps/portal/src/components/recipes/TokenSelectDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ const TokenSelectDialogItem = (props: TokenSelectDialogItemProps) => {

const value = useMemo(() => {
if (props.rates === undefined) return null
const balance = fastBalance?.balance ?? props.defaultBalanceDecimal
const balance = fastBalance?.balance?.transferrable ?? props.defaultBalanceDecimal
if (balance === undefined) return <Skeleton.Surface className="ml-auto h-[18px] w-[50px]" />
return (props.rates * balance.toNumber()).toLocaleString(undefined, { currency, style: 'currency' })
}, [currency, fastBalance, props.defaultBalanceDecimal, props.rates])

const balanceUI = useMemo(() => {
const balance = fastBalance?.balance ?? props.defaultBalanceDecimal
const balance = fastBalance?.balance?.transferrable ?? props.defaultBalanceDecimal
if (balance === undefined) return <Skeleton.Surface className="h-[21px] w-[70px]" />
return balance.toLocaleString(undefined, { maximumFractionDigits: 4 })
}, [fastBalance?.balance, props.defaultBalanceDecimal])
Expand Down
72 changes: 60 additions & 12 deletions apps/portal/src/components/widgets/chainflip-swap/FromAmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,25 @@ import { selectedCurrencyState } from '@/domains/balances'
import { cn } from '@/lib/utils'
import { useTokenRates } from '@talismn/balances-react'
import { Decimal } from '@talismn/math'
import { CircularProgressIndicator, TextInput } from '@talismn/ui'
import { CircularProgressIndicator, TextInput, Tooltip } from '@talismn/ui'
import { useAtom, useAtomValue } from 'jotai'
import { HelpCircle } from 'lucide-react'
import { useCallback, useMemo } from 'react'
import { Link } from 'react-router-dom'
import { useRecoilValue } from 'recoil'

const hardcodedGasBufferByTokenSymbol: Record<string, number> = {
dot: 0.03,
eth: 0.01, // same as uniswap, they give a fixed 0.01 ETH buffer regardless of the chain
}

export const FromAmount: React.FC<{
// NOTE: we get this as a prop so we dont have to get this balance twice. The parent component also needs this to
// check whether user has enough balance to swap
availableBalance: { balance: Decimal; loading: boolean } | null
wouldReapAccount?: boolean
availableBalance?: Decimal
stayAliveBalance?: Decimal
insufficientBalance?: boolean
}> = ({ availableBalance, insufficientBalance }) => {
}> = ({ availableBalance, insufficientBalance, stayAliveBalance }) => {
const [fromAmountInput, setFromAmountInput] = useAtom(fromAmountInputAtom)
const [fromAsset, setFromAsset] = useAtom(fromAssetAtom)
const [toAsset, setToAsset] = useAtom(toAssetAtom)
Expand Down Expand Up @@ -52,16 +59,35 @@ export const FromAmount: React.FC<{
return +fromAmount.toString() * rateInCurrency
}, [currency, fromAmount, fromAsset, rates])

const accountWouldBeReaped = useMemo(() => {
if (!stayAliveBalance) return false
return stayAliveBalance < fromAmount
}, [fromAmount, stayAliveBalance])

const maxAfterGas = useMemo(() => {
if (!fromAsset || !availableBalance) return null
const idParts = fromAsset.id.split('-')
const assetType = idParts[idParts.length - 1]
if (assetType === 'native') {
const gasBuffer = hardcodedGasBufferByTokenSymbol[fromAsset.symbol.toLowerCase()]
if (gasBuffer) {
const gasBufferDecimal = Decimal.fromUserInputOrUndefined(gasBuffer?.toString(), fromAsset.decimals)
return Decimal.fromPlanck(availableBalance.planck - (gasBufferDecimal?.planck ?? 0n), fromAsset.decimals)
}
}
return availableBalance
}, [availableBalance, fromAsset])

return (
<TextInput
autoComplete="off"
leadingLabel="You're paying"
trailingLabel={
availableBalance ? (
availableBalance.loading ? (
<CircularProgressIndicator size={12} />
fromAsset && fromAddress ? (
availableBalance ? (
`Balance: ${availableBalance.toLocaleString()}`
) : (
`Balance: ${availableBalance.balance.toLocaleString()}`
<CircularProgressIndicator size={12} />
)
) : null
}
Expand All @@ -74,6 +100,30 @@ export const FromAmount: React.FC<{
<p className="text-red-400 text-[10px] leading-none pl-[8px] ml-[8px] border-l border-l-gray-600">
Insufficient balance
</p>
) : accountWouldBeReaped ? (
<div className="flex items-center gap-1 text-orange-400">
<p className="text-[10px] leading-none pl-[8px] ml-[8px] border-l border-l-gray-600">
Account would be reaped
</p>

<Tooltip
placement="bottom"
content={
<p className="text-[12px]">
This amount will cause your balance to go below the network's{' '}
<span className="text-white">Existential Deposit</span>,
<br />
which would cause your account to be reaped.
<br />
Any remaining funds in a reaped account cannot be recovered.
</p>
}
>
<Link to="https://support.polkadot.network/support/solutions/articles/65000168651-what-is-the-existential-deposit-">
<HelpCircle className="w-4 h-4" />
</Link>
</Tooltip>
</div>
) : null}
</div>
}
Expand All @@ -91,13 +141,11 @@ export const FromAmount: React.FC<{
type="number"
trailingIcon={
<div className="flex items-center gap-[8px] justify-end">
{availableBalance && !availableBalance.loading && (
{maxAfterGas && maxAfterGas.planck > 0 && (
<TextInput.LabelButton
css={{ fontSize: 12, paddingTop: 4, paddingBottom: 4 }}
onClick={() =>
setFromAmountInput(
Decimal.fromPlanck(availableBalance.balance.planck, availableBalance.balance.decimals).toString()
)
setFromAmountInput(Decimal.fromPlanck(maxAfterGas.planck, maxAfterGas.decimals).toString())
}
>
<p css={{ fontSize: 12, lineHeight: 1 }}>Max</p>
Expand Down
39 changes: 12 additions & 27 deletions apps/portal/src/components/widgets/chainflip-swap/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
} from './swaps.api'
import { useFastBalance } from '@/hooks/useFastBalance'
import { isAddress as isSubstrateAddress } from '@polkadot/util-crypto'
import { Decimal } from '@talismn/math'
import { Button, Surface, TonalIconButton } from '@talismn/ui'
import { Repeat } from '@talismn/web-icons'
import { useAtom, useAtomValue, useSetAtom } from 'jotai'
Expand Down Expand Up @@ -83,28 +82,10 @@ export const ChainFlipSwap: React.FC = () => {
}, [fromAddress, fromAsset])
)

const availableBalance = useMemo(() => {
if (!fromAddress || !fromToken || !fastBalance) return null

if (
fastBalance.balance === undefined ||
fromToken.symbol.toLowerCase() !== fastBalance?.balance?.options?.currency?.toLowerCase()
) {
return {
balance: Decimal.fromPlanck(0n, fromToken.decimals, { currency: fromToken.symbol }),
loading: true,
}
}
return {
balance: fastBalance?.balance ?? Decimal.fromPlanck(0n, fromToken.decimals, { currency: fromToken.symbol }),
loading: fastBalance?.balance === undefined,
}
}, [fastBalance, fromAddress, fromToken])

const insufficientBalance = useMemo(() => {
if (!availableBalance || availableBalance.loading) return undefined
return fromAmount.planck > availableBalance.balance.planck
}, [availableBalance, fromAmount.planck])
if (!fastBalance?.balance) return undefined
return fromAmount.planck > fastBalance.balance.transferrable.planck
}, [fastBalance, fromAmount.planck])

useEffect(() => {
if (fromAmountInput.trim() !== '' && fromAsset && toAsset) setShouldFocusDetails(true)
Expand Down Expand Up @@ -134,7 +115,11 @@ export const ChainFlipSwap: React.FC = () => {
<div className="grid gap-[8px] w-full relative">
<Surface className="bg-card p-[16px] rounded-[8px] w-full">
<h4 className="text-[18px] font-semibold mb-[8px]">Select Asset</h4>
<FromAmount availableBalance={availableBalance} insufficientBalance={insufficientBalance} />
<FromAmount
availableBalance={fastBalance?.balance?.transferrable}
stayAliveBalance={fastBalance?.balance?.stayAlive}
insufficientBalance={insufficientBalance}
/>
<div className="relative w-full h-[12px]">
<TonalIconButton
className="border-3 !border-solid !border-gray-900 -top-[8px] absolute z-10 left-1/2 -translate-x-1/2 !bg-[#2D3121] !w-[48px] !h-[48px] !rounded-full"
Expand All @@ -147,9 +132,9 @@ export const ChainFlipSwap: React.FC = () => {
</Surface>
<FromAccount
fastBalance={
fromAsset && availableBalance
fromAsset && fastBalance?.balance
? {
amount: availableBalance.balance,
amount: fastBalance?.balance.transferrable,
chainId: fromAsset.chainId,
}
: undefined
Expand Down Expand Up @@ -183,8 +168,8 @@ export const ChainFlipSwap: React.FC = () => {
loading={swapping}
onClick={() => {
setInfoTab('details')
if (quote.state === 'hasData' && quote.data) {
swap(quote.data.protocol)
if (quote.state === 'hasData' && quote.data && fastBalance?.balance) {
swap(quote.data.protocol, fromAmount.planck > fastBalance.balance.stayAlive.planck)
}
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ const saveAddressForQuest = async (swapId: string, fromAddress: string) => {
const swap: SwapFunction<ChainflipSwapActivityData> = async (
get: Getter,
_: Setter,
{ evmWalletClient, getSubstrateApi, substrateWallet }
{ evmWalletClient, getSubstrateApi, substrateWallet, allowReap }
) => {
const sdk = get(swapSdkAtom)
const network = get(chainflipNetworkAtom)
Expand Down Expand Up @@ -308,9 +308,10 @@ const swap: SwapFunction<ChainflipSwapActivityData> = async (
const polkadotRpc = get(polkadotRpcAtom)
const polkadotApi = await getSubstrateApi(polkadotRpc)

await polkadotApi.tx.balances
.transferKeepAlive(depositAddress.depositAddress, depositAddress.amount)
.signAndSend(fromAddress, { signer, withSignedTransaction: true })
await polkadotApi.tx.balances[allowReap ? 'transferAllowDeath' : 'transferKeepAlive'](
depositAddress.depositAddress,
depositAddress.amount
).signAndSend(fromAddress, { signer, withSignedTransaction: true })

saveAddressForQuest(depositAddress.depositChannelId, fromAddress)
return { protocol: PROTOCOL, data: { id: depositAddress.depositChannelId, network } }
Expand Down Expand Up @@ -357,7 +358,7 @@ const getEstimateGasTx: GetEstimateGasTxFunction = async (get, { getSubstrateApi
return {
type: 'substrate',
fromAddress,
tx: polkadotApi.tx.balances.transferKeepAlive(fromAddress, fromAmount.planck),
tx: polkadotApi.tx.balances.transferAllowDeath(fromAddress, fromAmount.planck),
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type SwapProps = {
evmWalletClient?: WalletClient
substrateWallet?: BaseWallet
getSubstrateApi: (rpc: string) => Promise<ApiPromise>
allowReap?: boolean
toAmount: Decimal | null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const useSwap = () => {

const swap = useAtomCallback(
useCallback(
async (get, set, protocol: SupportedSwapProtocol) => {
async (get, set, protocol: SupportedSwapProtocol, allowReap = false) => {
try {
set(swappingAtom, true)
const toAmount = await get(toAmountAtom)
Expand All @@ -181,6 +181,7 @@ export const useSwap = () => {
substrateWallet,
getSubstrateApi,
toAmount,
allowReap,
})
// TODO: instead of just getting "swapped: boolean"
// we should handle adding swap to activity generically so that
Expand Down
10 changes: 8 additions & 2 deletions apps/portal/src/hooks/useFastBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,16 @@ export const useFastBalance = (props?: UseFastBalanceProps) => {
balance: substrateBalance,
}
}
const ethBalance = evmBalance.data
? Decimal.fromPlanck(evmBalance.data.value, evmBalance.data.decimals, { currency: evmBalance.data.symbol })
: undefined
return {
...props,
balance: evmBalance.data
? Decimal.fromPlanck(evmBalance.data.value, evmBalance.data.decimals, { currency: evmBalance.data.symbol })
balance: ethBalance
? {
transferrable: ethBalance,
stayAlive: ethBalance,
}
: undefined,
}
}, [evmBalance.data, props, substrateBalance])
Expand Down
15 changes: 10 additions & 5 deletions apps/portal/src/hooks/useSubstrateBalance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ export type UseSubstrateBalanceProps = {
address: string
}

type SubstrateBalance = {
transferrable: Decimal
stayAlive: Decimal
}

export const useSubstrateBalance = (props?: UseSubstrateBalanceProps) => {
const [balance, setBalance] = useState<Decimal | undefined>()
const [balance, setBalance] = useState<SubstrateBalance | undefined>()
const unsubRef = useRef<() => void>()
const chainsLoadable = useRecoilValueLoadable(chainsState)
const api = useRecoilValueLoadable(
Expand All @@ -34,14 +39,14 @@ export const useSubstrateBalance = (props?: UseSubstrateBalanceProps) => {
const reserved = account.data.reserved.toBigInt()
const frozen = account.data.frozen.toBigInt()
const untouchable = BigMath.max(frozen - reserved, 0n)
// make sure untouchable is never less than the existentialDeposit
const untouchableOrEd = BigMath.max(untouchable, ed.toBigInt())
const free = account.data.free.toBigInt()
const transferableBN = BigMath.max(free - untouchableOrEd, 0n)
const transferableBN = BigMath.max(free - untouchable, 0n)

const decimals = api.contents.registry.chainDecimals[0] ?? 10
const symbol = api.contents.registry.chainTokens[0] ?? 'DOT'
setBalance(Decimal.fromPlanck(transferableBN, decimals, { currency: symbol }))
const transferrable = Decimal.fromPlanck(transferableBN, decimals, { currency: symbol })
const stayAlive = Decimal.fromPlanck(free - ed.toBigInt(), decimals, { currency: symbol })
setBalance({ transferrable, stayAlive })
})
.then(unsub => {
unsubRef.current = unsub
Expand Down
1 change: 0 additions & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
"dev": "tsc --build --watch tsconfig.build.json",
"build": "rm -rf build && tsc --build tsconfig.build.json",
"lint": "eslint src",
"check-types": "tsc --noEmit",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
Expand Down
Loading