251 lines
7.0 KiB
TypeScript
251 lines
7.0 KiB
TypeScript
import * as assert from "assert";
|
|
import React, { useContext, useState, useEffect } from "react";
|
|
import { useAsync } from "react-async-hook";
|
|
import { PublicKey } from "@solana/web3.js";
|
|
import {
|
|
Token,
|
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
TOKEN_PROGRAM_ID,
|
|
} from "@solana/spl-token";
|
|
import { Market } from "@project-serum/serum";
|
|
import { SRM_MINT, USDC_MINT, USDT_MINT } from "../utils/pubkeys";
|
|
import {
|
|
useFairRoute,
|
|
useRouteVerbose,
|
|
useDexContext,
|
|
FEE_MULTIPLIER,
|
|
} from "./Dex";
|
|
import {
|
|
useTokenListContext,
|
|
SPL_REGISTRY_SOLLET_TAG,
|
|
SPL_REGISTRY_WORM_TAG,
|
|
} from "./TokenList";
|
|
import { useOwnedTokenAccount } from "../context/Token";
|
|
|
|
const DEFAULT_SLIPPAGE_PERCENT = 0.5;
|
|
|
|
export type SwapContext = {
|
|
// Mint being traded from. The user must own these tokens.
|
|
fromMint: PublicKey;
|
|
setFromMint: (m: PublicKey) => void;
|
|
|
|
// Mint being traded to. The user will receive these tokens after the swap.
|
|
toMint: PublicKey;
|
|
setToMint: (m: PublicKey) => void;
|
|
|
|
// Amount used for the swap.
|
|
fromAmount: number;
|
|
setFromAmount: (a: number) => void;
|
|
|
|
// *Expected* amount received from the swap.
|
|
toAmount: number;
|
|
setToAmount: (a: number) => void;
|
|
|
|
// Function to flip what we consider to be the "to" and "from" mints.
|
|
swapToFromMints: () => void;
|
|
|
|
// The amount (in units of percent) a swap can be off from the estimate
|
|
// shown to the user.
|
|
slippage: number;
|
|
setSlippage: (n: number) => void;
|
|
|
|
// Null if the user is using fairs directly from DEX prices.
|
|
// Otherwise, a user specified override for the price to use when calculating
|
|
// swap amounts.
|
|
fairOverride: number | null;
|
|
setFairOverride: (n: number | null) => void;
|
|
|
|
// The referral *owner* address. Associated token accounts must be created,
|
|
// first, for this to be used.
|
|
referral?: PublicKey;
|
|
|
|
// True if all newly created market accounts should be closed in the
|
|
// same user flow (ideally in the same transaction).
|
|
isClosingNewAccounts: boolean;
|
|
|
|
// True if the swap exchange rate should be a function of nothing but the
|
|
// from and to tokens, ignoring any quote tokens that may have been
|
|
// accumulated by performing the swap.
|
|
//
|
|
// Always false (for now).
|
|
isStrict: boolean;
|
|
setIsStrict: (isStrict: boolean) => void;
|
|
|
|
setIsClosingNewAccounts: (b: boolean) => void;
|
|
};
|
|
const _SwapContext = React.createContext<null | SwapContext>(null);
|
|
|
|
export function SwapContextProvider(props: any) {
|
|
const [fromMint, setFromMint] = useState(props.fromMint ?? SRM_MINT);
|
|
const [toMint, setToMint] = useState(props.toMint ?? USDC_MINT);
|
|
const [fromAmount, _setFromAmount] = useState(props.fromAmount ?? 0);
|
|
const [toAmount, _setToAmount] = useState(props.toAmount ?? 0);
|
|
const [isClosingNewAccounts, setIsClosingNewAccounts] = useState(false);
|
|
const [isStrict, setIsStrict] = useState(false);
|
|
const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE_PERCENT);
|
|
const [fairOverride, setFairOverride] = useState<number | null>(null);
|
|
const fair = _useSwapFair(fromMint, toMint, fairOverride);
|
|
const referral = props.referral;
|
|
|
|
assert.ok(slippage >= 0);
|
|
|
|
useEffect(() => {
|
|
if (!fair) {
|
|
return;
|
|
}
|
|
setFromAmount(fromAmount);
|
|
}, [fair]);
|
|
|
|
const swapToFromMints = () => {
|
|
const oldFrom = fromMint;
|
|
const oldTo = toMint;
|
|
const oldToAmount = toAmount;
|
|
_setFromAmount(oldToAmount);
|
|
setFromMint(oldTo);
|
|
setToMint(oldFrom);
|
|
};
|
|
|
|
const setFromAmount = (amount: number) => {
|
|
if (fair === undefined) {
|
|
_setFromAmount(0);
|
|
_setToAmount(0);
|
|
return;
|
|
}
|
|
_setFromAmount(amount);
|
|
_setToAmount(FEE_MULTIPLIER * (amount / fair));
|
|
};
|
|
|
|
const setToAmount = (amount: number) => {
|
|
if (fair === undefined) {
|
|
_setFromAmount(0);
|
|
_setToAmount(0);
|
|
return;
|
|
}
|
|
_setToAmount(amount);
|
|
_setFromAmount((amount * fair) / FEE_MULTIPLIER);
|
|
};
|
|
|
|
return (
|
|
<_SwapContext.Provider
|
|
value={{
|
|
fromMint,
|
|
setFromMint,
|
|
toMint,
|
|
setToMint,
|
|
fromAmount,
|
|
setFromAmount,
|
|
toAmount,
|
|
setToAmount,
|
|
swapToFromMints,
|
|
slippage,
|
|
setSlippage,
|
|
fairOverride,
|
|
setFairOverride,
|
|
isClosingNewAccounts,
|
|
isStrict,
|
|
setIsStrict,
|
|
setIsClosingNewAccounts,
|
|
referral,
|
|
}}
|
|
>
|
|
{props.children}
|
|
</_SwapContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function useSwapContext(): SwapContext {
|
|
const ctx = useContext(_SwapContext);
|
|
if (ctx === null) {
|
|
throw new Error("Context not available");
|
|
}
|
|
return ctx;
|
|
}
|
|
|
|
export function useSwapFair(): number | undefined {
|
|
const { fairOverride, fromMint, toMint } = useSwapContext();
|
|
return _useSwapFair(fromMint, toMint, fairOverride);
|
|
}
|
|
|
|
function _useSwapFair(
|
|
fromMint: PublicKey,
|
|
toMint: PublicKey,
|
|
fairOverride: number | null
|
|
): number | undefined {
|
|
const fairRoute = useFairRoute(fromMint, toMint);
|
|
const fair = fairOverride === null ? fairRoute : fairOverride;
|
|
return fair;
|
|
}
|
|
|
|
// Returns true if the user can swap with the current context.
|
|
export function useCanSwap(): boolean {
|
|
const { fromMint, toMint, fromAmount, toAmount } = useSwapContext();
|
|
const { swapClient } = useDexContext();
|
|
const { wormholeMap, solletMap } = useTokenListContext();
|
|
const fromWallet = useOwnedTokenAccount(fromMint);
|
|
const fair = useSwapFair();
|
|
const route = useRouteVerbose(fromMint, toMint);
|
|
if (route === null) {
|
|
return false;
|
|
}
|
|
|
|
return (
|
|
// From wallet exists.
|
|
fromWallet !== undefined &&
|
|
fromWallet !== null &&
|
|
// Fair price is defined.
|
|
fair !== undefined &&
|
|
fair > 0 &&
|
|
// Mints are distinct.
|
|
fromMint.equals(toMint) === false &&
|
|
// Wallet is connected.
|
|
swapClient.program.provider.wallet.publicKey !== null &&
|
|
// Trade amounts greater than zero.
|
|
fromAmount > 0 &&
|
|
toAmount > 0 &&
|
|
// Trade route exists.
|
|
route !== null &&
|
|
// Wormhole <-> native markets must have the wormhole token as the
|
|
// *from* address since they're one-sided markets.
|
|
(route.kind !== "wormhole-native" ||
|
|
wormholeMap
|
|
.get(fromMint.toString())
|
|
?.tags?.includes(SPL_REGISTRY_WORM_TAG) !== undefined) &&
|
|
// Wormhole <-> sollet markets must have the sollet token as the
|
|
// *from* address since they're one sided markets.
|
|
(route.kind !== "wormhole-sollet" ||
|
|
solletMap
|
|
.get(fromMint.toString())
|
|
?.tags?.includes(SPL_REGISTRY_SOLLET_TAG) !== undefined)
|
|
);
|
|
}
|
|
|
|
export function useReferral(fromMarket?: Market): PublicKey | undefined {
|
|
const { referral } = useSwapContext();
|
|
const asyncReferral = useAsync(async () => {
|
|
if (!referral) {
|
|
return undefined;
|
|
}
|
|
if (!fromMarket) {
|
|
return undefined;
|
|
}
|
|
if (
|
|
!fromMarket.quoteMintAddress.equals(USDC_MINT) &&
|
|
!fromMarket.quoteMintAddress.equals(USDT_MINT)
|
|
) {
|
|
return undefined;
|
|
}
|
|
|
|
return Token.getAssociatedTokenAddress(
|
|
ASSOCIATED_TOKEN_PROGRAM_ID,
|
|
TOKEN_PROGRAM_ID,
|
|
fromMarket.quoteMintAddress,
|
|
referral
|
|
);
|
|
}, [fromMarket]);
|
|
|
|
if (!asyncReferral.result) {
|
|
return undefined;
|
|
}
|
|
return asyncReferral.result;
|
|
}
|