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); 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(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} ); } 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; }