Skip to content

Commit f75c7f2

Browse files
committed
feat: add configurable slippage and better error toest - WIP
1 parent fd5cb04 commit f75c7f2

File tree

9 files changed

+171
-60
lines changed

9 files changed

+171
-60
lines changed

apps/namadillo/src/App/Swap/SwapCalculations.tsx

Lines changed: 47 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ import { wallets } from "integrations";
1919
import { KeplrWalletManager } from "integrations/Keplr";
2020
import { getChainFromAddress } from "integrations/utils";
2121
import { useAtom, useAtomValue, useSetAtom } from "jotai";
22-
import { useCallback, useEffect, useRef, useState } from "react";
23-
import { NamadaAsset } from "types";
22+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
23+
import { toDisplayAmount } from "utils";
2424
import { SwapSource } from "./SwapSource";
2525
import { useSwapSimulation } from "./hooks/useSwapSimulation";
2626
import { useSwapValidation } from "./hooks/useSwapValidation";
2727
import { SwapQuote, SwapState, SwapStatus } from "./state";
28-
import { swapQuoteAtom, swapStateAtom, swapStatusAtom } from "./state/atoms";
28+
import {
29+
swapMinAmountAtom,
30+
swapQuoteAtom,
31+
swapStateAtom,
32+
swapStatusAtom,
33+
} from "./state/atoms";
2934

3035
const ValidationMessages: Record<string, string> = {
3136
NoSellAssetSelected: "Select a token to sell",
@@ -54,6 +59,7 @@ export const SwapCalculations = (): JSX.Element => {
5459
const [swapState, setSwapState] = useAtom(swapStateAtom);
5560
const { data: quote } = useAtomValue(swapQuoteAtom);
5661
const setStatus = useSetAtom(swapStatusAtom);
62+
const minAmount = useAtomValue(swapMinAmountAtom);
5763

5864
// Global state
5965
const sortedAssets = useAtomValue(namadaAssetsSortedAtom);
@@ -111,10 +117,9 @@ export const SwapCalculations = (): JSX.Element => {
111117
sellAmount: a,
112118
}));
113119
} else {
114-
setSwapState((s) => ({
120+
setSwapState({
115121
mode: "none",
116-
sellAmountPerOneBuy: s.sellAmountPerOneBuy,
117-
}));
122+
});
118123
}
119124
}, []);
120125

@@ -126,10 +131,9 @@ export const SwapCalculations = (): JSX.Element => {
126131
buyAmount: a,
127132
}));
128133
} else {
129-
setSwapState((s) => ({
134+
setSwapState({
130135
mode: "none",
131-
sellAmountPerOneBuy: s.sellAmountPerOneBuy,
132-
}));
136+
});
133137
}
134138
}, []);
135139

@@ -141,7 +145,6 @@ export const SwapCalculations = (): JSX.Element => {
141145
mode: newMode,
142146
sellAmount: s.buyAmount,
143147
buyAmount: s.sellAmount,
144-
sellAmountPerOneBuy: s.sellAmountPerOneBuy,
145148
};
146149
}
147150

@@ -240,20 +243,15 @@ export const SwapCalculations = (): JSX.Element => {
240243
isSubmitting={false}
241244
label="Buy"
242245
/>
243-
{feeProps &&
244-
swapState.sellAmountPerOneBuy &&
245-
sellAsset &&
246-
buyAsset &&
247-
tokenPrices && (
248-
<SwapCalculationsFooter
249-
feeProps={feeProps}
250-
sellAmountPerOneBuy={swapState.sellAmountPerOneBuy}
251-
selectedAsset={sellAsset}
252-
selectedTargetAsset={buyAsset}
253-
tokenPrice={tokenPrices[buyAsset.address]}
254-
quote={quote}
255-
/>
256-
)}
246+
{feeProps && sellAsset && buyAsset && tokenPrices && (
247+
<SwapCalculationsFooter
248+
feeProps={feeProps}
249+
swapState={swapState}
250+
minAmount={minAmount}
251+
tokenPrice={tokenPrices[buyAsset.address]}
252+
quote={quote}
253+
/>
254+
)}
257255

258256
<ActionButton
259257
outlineColor="yellow"
@@ -299,21 +297,20 @@ export const SwapCalculations = (): JSX.Element => {
299297

300298
type SwapCalculationsFooterProps = {
301299
feeProps: TransactionFeeProps;
302-
sellAmountPerOneBuy: BigNumber;
303-
selectedAsset: NamadaAsset;
304-
selectedTargetAsset: NamadaAsset;
305300
tokenPrice: BigNumber;
306301
quote?: SwapQuote;
302+
swapState: SwapState;
303+
minAmount?: BigNumber;
307304
};
308305

309306
const SwapCalculationsFooter = ({
310307
feeProps,
311-
sellAmountPerOneBuy,
312-
selectedAsset,
313-
selectedTargetAsset,
308+
swapState,
314309
tokenPrice,
315310
quote,
311+
minAmount,
316312
}: SwapCalculationsFooterProps): JSX.Element => {
313+
const { sellAsset, buyAsset } = swapState;
317314
// Quote cache, prevents blinking when quote is temporarily undefined
318315
const lastValidQuoteRef = useRef<typeof quote>();
319316
useEffect(() => {
@@ -323,7 +320,24 @@ const SwapCalculationsFooter = ({
323320
}, [quote]);
324321

325322
const quoteToUse = quote ?? lastValidQuoteRef.current;
326-
if (!quoteToUse) {
323+
324+
const sellAmountPerOneBuy = useMemo(() => {
325+
if (!quoteToUse || !buyAsset || !minAmount) {
326+
return;
327+
}
328+
329+
const baseAmount =
330+
["sell", "none"].includes(swapState.mode) ?
331+
quoteToUse.amountIn
332+
: quoteToUse.amountOut;
333+
334+
return toDisplayAmount(
335+
buyAsset,
336+
minAmount.div(toDisplayAmount(buyAsset, baseAmount))
337+
);
338+
}, [buyAsset?.symbol, quoteToUse]);
339+
340+
if (!quoteToUse || !sellAmountPerOneBuy || !sellAsset || !buyAsset) {
327341
return <></>;
328342
}
329343

@@ -338,8 +352,8 @@ const SwapCalculationsFooter = ({
338352
direction="horizontal"
339353
>
340354
<div className="underline">
341-
1 {selectedAsset.symbol}{sellAmountPerOneBuy.toFixed(6)}{" "}
342-
{selectedTargetAsset.symbol} (${valFiat.toFixed(6)})
355+
1 {sellAsset.symbol}{sellAmountPerOneBuy.toFixed(6)}{" "}
356+
{buyAsset.symbol} (${valFiat.toFixed(6)})
343357
</div>
344358
<TransactionFeeButton
345359
compact={true}

apps/namadillo/src/App/Swap/SwapReview.tsx

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,22 @@ import { toDisplayAmount } from "utils";
2020
import { usePerformOsmosisSwapTx } from "./hooks/usePerformOsmosisSwapTx";
2121
import { useSwapReviewValidation } from "./hooks/useSwapReviewValidation";
2222
import { statusMessages, SwapStatus } from "./state";
23-
import { swapQuoteAtom, swapStateAtom, swapStatusAtom } from "./state/atoms";
24-
import { SLIPPAGE } from "./state/functions";
23+
import {
24+
swapMinAmountAtom,
25+
swapQuoteAtom,
26+
swapSlippageAtom,
27+
swapStateAtom,
28+
swapStatusAtom,
29+
} from "./state/atoms";
2530

2631
const keplr = new KeplrWalletManager();
2732
export const SwapReview = (): JSX.Element => {
2833
// Feature state sanity checks
2934
const [status, setStatus] = useAtom(swapStatusAtom);
3035
const swapState = useAtomValue(swapStateAtom);
36+
const minAmount = useAtomValue(swapMinAmountAtom);
37+
const [{ default: slippage, override: slippageOverride }, setSlippage] =
38+
useAtom(swapSlippageAtom);
3139
const { sellAsset, buyAsset } = swapState;
3240
const { data: quote } = useAtomValue(swapQuoteAtom);
3341

@@ -44,6 +52,7 @@ export const SwapReview = (): JSX.Element => {
4452
invariant(quote, "Quote is required");
4553
invariant(swapState.sellAmount, "Swap sell amount is required");
4654
invariant(swapState.buyAmount, "Swap buy amount is required");
55+
invariant(minAmount, "Minimum amount is required");
4756

4857
// Global state
4958
const [ledgerStatus, setLedgerStatusStop] = useAtom(ledgerStatusDataAtom);
@@ -58,7 +67,7 @@ export const SwapReview = (): JSX.Element => {
5867
buyPrice && buyPrice.times(BigNumber(1).plus(quote.priceImpact));
5968
const buyAmountFiat =
6069
buyPriceImpact && buyPriceImpact.times(swapState.buyAmount);
61-
const receiveAtLeastDenominated = toDisplayAmount(buyAsset, quote.minAmount);
70+
const receiveAtLeastDenominated = toDisplayAmount(buyAsset, minAmount);
6271

6372
const swapFee = quote.effectiveFee
6473
.times(100)
@@ -75,7 +84,9 @@ export const SwapReview = (): JSX.Element => {
7584

7685
const { error: _err, performSwap } = usePerformOsmosisSwapTx();
7786
const onSwap = useCallback(async (): Promise<void> => {
78-
await performSwap({ localRecoveryAddr: walletAddress });
87+
await performSwap({
88+
localRecoveryAddr: walletAddress,
89+
});
7990
}, [walletAddress]);
8091

8192
const validationResult = useSwapReviewValidation({
@@ -147,8 +158,34 @@ export const SwapReview = (): JSX.Element => {
147158
</p>
148159
</ReviewRow>
149160
<ReviewRow>
150-
<div>Slippage tolerance</div>
151-
<div>{SLIPPAGE * 100}%</div>
161+
<div className="self-center">Slippage tolerance</div>
162+
<div className="relative inline-block">
163+
<input
164+
type="text"
165+
placeholder={slippage.times(100).toString()}
166+
className="peer h-full pl-3 pr-4 w-16 placeholder-yellow-600 text-yellow text-right bg-transparent outline-none border border-transparent rounded-sm focus:py-2 focus:pr-7 focus:border-yellow transition-all"
167+
onChange={(e) => {
168+
const val = e.target.value;
169+
if (val === "" || val.match(/^\d{1}(\.\d{0,1})?$/)) {
170+
setSlippage(BigNumber(val).div(100));
171+
}
172+
}}
173+
value={
174+
slippageOverride ?
175+
slippageOverride.times(100).toString()
176+
: ""
177+
}
178+
/>
179+
<span
180+
className={clsx(
181+
"absolute right-0 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none peer-focus:right-3 transition-all",
182+
{ "text-yellow": !!slippageOverride },
183+
{ "text-yellow-600": !slippageOverride }
184+
)}
185+
>
186+
%
187+
</span>
188+
</div>
152189
</ReviewRow>
153190
<ReviewRow>
154191
<div>Receive at least</div>

apps/namadillo/src/App/Swap/hooks/usePerformOsmosisSwapTx.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ import { NamadaAsset, OsmosisSwapTransactionData, TransferStep } from "types";
2323
import { TransactionError } from "types/errors";
2424
import { toBaseAmount } from "utils";
2525
import { SwapStatus } from "../state";
26-
import { swapQuoteAtom, swapStateAtom, swapStatusAtom } from "../state/atoms";
26+
import {
27+
swapMinAmountAtom,
28+
swapQuoteAtom,
29+
swapStateAtom,
30+
swapStatusAtom,
31+
} from "../state/atoms";
2732

2833
// TODO: Should be a different address for housefire
2934
const SWAP_CONTRACT_ADDRESS =
@@ -50,6 +55,7 @@ export function usePerformOsmosisSwapTx(): UsePerformOsmosisSwapResult {
5055
const { buyAmount, sellAmount, buyAsset, sellAsset } =
5156
useAtomValue(swapStateAtom);
5257
const quoteQuery = useAtomValue(swapQuoteAtom);
58+
const minAmount = useAtomValue(swapMinAmountAtom);
5359

5460
// Global state
5561
const namadaChain = useAtomValue(chainAtom);
@@ -126,6 +132,7 @@ export function usePerformOsmosisSwapTx(): UsePerformOsmosisSwapResult {
126132
invariant(sellAmount, "No sell amount");
127133
invariant(buyAmount, "No buy amount");
128134
invariant(sellAsset && buyAsset, "Missing swap assets");
135+
invariant(minAmount, "No minimum amount calculated");
129136

130137
const toTrace =
131138
buyAsset.traces?.find((t) => t.type === "ibc")?.chain.path ||
@@ -154,9 +161,7 @@ export function usePerformOsmosisSwapTx(): UsePerformOsmosisSwapResult {
154161
recipient: shieldedAccount.address,
155162
overflow: transparentAccount.address,
156163
slippage: {
157-
0: BigNumber(quote.minAmount)
158-
.integerValue(BigNumber.ROUND_DOWN)
159-
.toString(),
164+
0: minAmount.integerValue(BigNumber.ROUND_DOWN).toString(),
160165
},
161166
localRecoveryAddr,
162167
route,
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import BigNumber from "bignumber.js";
2+
import invariant from "invariant";
3+
import { useAtomValue } from "jotai";
4+
import { useEffect, useState } from "react";
5+
import { toDisplayAmount } from "utils";
6+
import { SwapQuote, SwapState } from "../state";
7+
import { swapMinAmountAtom } from "../state/atoms";
8+
9+
export const useSellPerOneBuy = (
10+
swapState: SwapState,
11+
quote?: SwapQuote
12+
): BigNumber | undefined => {
13+
const [sellAmountPerOneBuy, setSellAmountPerOneBuy] = useState<BigNumber>();
14+
const minAmount = useAtomValue(swapMinAmountAtom);
15+
16+
useEffect(() => {
17+
if (!quote || !swapState.buyAsset) return;
18+
19+
invariant(swapState.buyAsset, "Buy asset is required for calculation");
20+
invariant(minAmount, "Min amount is required for calculation");
21+
22+
const baseAmount =
23+
["sell", "none"].includes(swapState.mode) ?
24+
quote.amountIn
25+
: quote.amountOut;
26+
27+
const sellAmountPerOneBuy = toDisplayAmount(
28+
swapState.buyAsset,
29+
minAmount.div(toDisplayAmount(swapState.buyAsset, baseAmount))
30+
);
31+
setSellAmountPerOneBuy(sellAmountPerOneBuy);
32+
}, [swapState, quote]);
33+
34+
return sellAmountPerOneBuy;
35+
};

apps/namadillo/src/App/Swap/hooks/useSwapSimulation.tsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,6 @@ export const useSwapSimulation = (): void => {
2323
invariant(buyAsset, "Buy asset is required for simulation");
2424
invariant(sellAsset, "Sell asset is required for simulation");
2525

26-
const baseAmount =
27-
swapState.mode === "sell" ? quote.amountIn : quote.amountOut;
28-
29-
const sellAmountPerOneBuy = toDisplayAmount(
30-
buyAsset,
31-
quote.minAmount.div(toDisplayAmount(buyAsset, baseAmount))
32-
);
33-
3426
const simulateSell =
3527
swapState.mode === "sell" || swapState.mode === "none";
3628
const simulateBuy = swapState.mode === "buy";
@@ -40,15 +32,13 @@ export const useSwapSimulation = (): void => {
4032
setInternalSwapState((s) => ({
4133
...s,
4234
buyAmount: toDisplayAmount(buyAsset, quote.amountOut),
43-
sellAmountPerOneBuy,
4435
}));
4536
}
4637
} else if (simulateBuy && buyAsset) {
4738
if (swapState.buyAmount === internalSwapState.buyAmount) {
4839
setInternalSwapState((s) => ({
4940
...s,
5041
sellAmount: toDisplayAmount(sellAsset, quote.amountIn),
51-
sellAmountPerOneBuy,
5242
}));
5343
}
5444
}

apps/namadillo/src/App/Swap/state/atoms.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { atomWithStorage } from "jotai/utils";
1515
import debounce from "lodash.debounce";
1616
import { SwapStorage } from "types";
1717
import { toBaseAmount } from "utils";
18-
import { fetchQuote } from "./functions";
18+
import { fetchQuote, SLIPPAGE } from "./functions";
1919
import type { SwapState, SwapStatusType } from "./types";
2020
import { SwapStatus } from "./types";
2121

@@ -189,3 +189,27 @@ export const swapQuoteAtom = atomWithQuery((get) => {
189189
},
190190
};
191191
});
192+
193+
const internalSwapSlippageAtom = atom<{
194+
default: BigNumber;
195+
override: BigNumber | null;
196+
}>({ default: BigNumber(SLIPPAGE), override: null });
197+
198+
export const swapSlippageAtom = atom(
199+
(get) => get(internalSwapSlippageAtom),
200+
(_get, set, slippage: BigNumber | null) => {
201+
set(internalSwapSlippageAtom, (s) => ({
202+
...s,
203+
override: slippage,
204+
}));
205+
}
206+
);
207+
208+
export const swapMinAmountAtom = atom<BigNumber | undefined>((get) => {
209+
const quote = get(swapQuoteAtom).data;
210+
const slippage = get(swapSlippageAtom);
211+
const slippageValue = slippage.override || slippage.default;
212+
213+
if (!quote) return;
214+
return quote.amount.times(BigNumber(1).minus(slippageValue));
215+
});

0 commit comments

Comments
 (0)