import React , { useContext , useState , useEffect } from "react" ; import * as assert from "assert" ; import { useAsync } from "react-async-hook" ; import { TokenInfo } from "@solana/spl-token-registry" ; import { MintLayout } from "@solana/spl-token" ; import { Connection , PublicKey } from "@solana/web3.js" ; import * as anchor from "@project-serum/anchor" ; import { Swap as SwapClient } from "@project-serum/swap" ; import { Market , OpenOrders , Orderbook as OrderbookSide , } from "@project-serum/serum" ; import { DEX_PID , USDC_MINT , USDT_MINT , SOL_MINT , WRAPPED_SOL_MINT , WORM_USDC_MINT , WORM_USDT_MINT , WORM_USDC_MARKET , WORM_USDT_MARKET , WORM_MARKET_BASE , } from "../utils/pubkeys" ; import { useTokenMap , useTokenListContext } from "./TokenList" ; import { fetchSolletInfo , requestWormholeSwapMarketIfNeeded } from "./Sollet" ; import { setMintCache } from "./Token" ; const BASE_TAKER_FEE_BPS = 0.0022 ; export const FEE_MULTIPLIER = 1 - BASE_TAKER_FEE_BPS ; type DexContext = { openOrders : Map < string , Array < OpenOrders > > ; closeOpenOrders : ( openOrder : OpenOrders ) => void ; swapClient : SwapClient ; } ; const _DexContext = React . createContext < DexContext | null > ( null ) ; export function DexContextProvider ( props ) { const [ ooAccounts , setOoAccounts ] = useState < Map < string , Array < OpenOrders > > > ( new Map ( ) ) ; const swapClient = props . swapClient ; const closeOpenOrders = async ( openOrder ) => { const newOoAccounts = new Map ( ooAccounts ) ; const openOrders = newOoAccounts . get ( openOrder . market . toString ( ) ) ?. filter ( ( oo ) => ! oo . address . equals ( openOrder . address ) ) ; if ( openOrders && openOrders . length > 0 ) { newOoAccounts . set ( openOrder . market . toString ( ) , openOrders ) ; } else { newOoAccounts . delete ( openOrder . market . toString ( ) ) ; } setOoAccounts ( newOoAccounts ) ; } ; useEffect ( ( ) => { if ( ! swapClient . program . provider . wallet . publicKey ) { setOoAccounts ( new Map ( ) ) ; return ; } OpenOrders . findForOwner ( swapClient . program . provider . connection , swapClient . program . provider . wallet . publicKey , DEX_PID ) . then ( async ( openOrders ) => { const newOoAccounts = new Map ( ) ; let markets = new Set < string > ( ) ; openOrders . forEach ( ( oo ) => { markets . add ( oo . market . toString ( ) ) ; if ( newOoAccounts . get ( oo . market . toString ( ) ) ) { newOoAccounts . get ( oo . market . toString ( ) ) . push ( oo ) ; } else { newOoAccounts . set ( oo . market . toString ( ) , [ oo ] ) ; } } ) ; if ( markets . size > 100 ) { throw new Error ( "Too many markets. Please file an issue to update this" ) ; } const multipleMarkets = await anchor . utils . rpc . getMultipleAccounts ( swapClient . program . provider . connection , Array . from ( markets . values ( ) ) . map ( ( m ) => new PublicKey ( m ) ) ) ; const marketClients = multipleMarkets . map ( ( programAccount ) => { return { publicKey : programAccount ?. publicKey , account : new Market ( Market . getLayout ( DEX_PID ) . decode ( programAccount ?. account . data ) , - 1 , - 1 , swapClient . program . provider . opts , DEX_PID ) , } ; } ) ; setOoAccounts ( newOoAccounts ) ; const mintPubkeys = Array . from ( new Set < string > ( marketClients . map ( ( m ) => [ m . account . baseMintAddress . toString ( ) , m . account . quoteMintAddress . toString ( ) , ] ) . flat ( ) ) . values ( ) ) . map ( ( pk ) => new PublicKey ( pk ) ) ; if ( mintPubkeys . length > 100 ) { throw new Error ( "Too many mints. Please file an issue to update this" ) ; } const mints = await anchor . utils . rpc . getMultipleAccounts ( swapClient . program . provider . connection , mintPubkeys ) ; const mintInfos = mints . map ( ( mint ) => { const mintInfo = MintLayout . decode ( mint ! . account . data ) ; setMintCache ( mint ! . publicKey , mintInfo ) ; return { publicKey : mint ! . publicKey , mintInfo } ; } ) ; marketClients . forEach ( ( m ) => { const baseMintInfo = mintInfos . filter ( ( mint ) => mint . publicKey . equals ( m . account . baseMintAddress ) ) [ 0 ] ; const quoteMintInfo = mintInfos . filter ( ( mint ) => mint . publicKey . equals ( m . account . quoteMintAddress ) ) [ 0 ] ; assert . ok ( baseMintInfo && quoteMintInfo ) ; m . account . _baseSplTokenDecimals = baseMintInfo . mintInfo . decimals ; m . account . _quoteSplTokenDecimals = quoteMintInfo . mintInfo . decimals ; _MARKET_CACHE . set ( m . publicKey ! . toString ( ) , new Promise < Market > ( ( resolve ) => resolve ( m . account ) ) ) ; } ) ; } ) ; } , [ swapClient . program . provider . connection , swapClient . program . provider . wallet . publicKey , swapClient . program . provider . opts , ] ) ; return ( < _DexContext . Provider value = { { openOrders : ooAccounts , closeOpenOrders , swapClient , } } > { props . children } < /_DexContext.Provider>); } export function useDexContext ( ) { const ctx = useContext ( _DexContext ) ; if ( ctx === null ) { throw new Error ( "Context not available" ) ; } return ctx ; } export function useOpenOrders ( ) { const ctx = useDexContext ( ) ; return ctx . openOrders ; } export function useMarket ( market ? ) { const { swapClient } = useDexContext ( ) ; const asyncMarket = useAsync ( async ( ) => { if ( ! market ) { return undefined ; } if ( _MARKET_CACHE . get ( market . toString ( ) ) ) { return _MARKET_CACHE . get ( market . toString ( ) ) ; } const marketClient = new Promise < Market > ( async ( resolve ) => { const marketClient = await Market . load ( swapClient . program . provider . connection , market , swapClient . program . provider . opts , DEX_PID ) ; resolve ( marketClient ) ; } ) ; _MARKET_CACHE . set ( market . toString ( ) , marketClient ) ; return marketClient ; } , [ swapClient . program . provider . connection , market ] ) ; if ( asyncMarket . result ) { return asyncMarket . result ; } return undefined ; } export function useOrderbook ( market ? ) { const { swapClient } = useDexContext ( ) ; const marketClient = useMarket ( market ) ; const [ refresh , setRefresh ] = useState ( 0 ) ; const asyncOrderbook = useAsync ( async ( ) => { if ( ! market || ! marketClient ) { return undefined ; } if ( _ORDERBOOK_CACHE . get ( market . toString ( ) ) ) { return _ORDERBOOK_CACHE . get ( market . toString ( ) ) ; } const orderbook = new Promise < Orderbook > ( async ( resolve ) => { const [ bids , asks ] = await Promise . all ( [ marketClient . loadBids ( swapClient . program . provider . connection ) , marketClient . loadAsks ( swapClient . program . provider . connection ) , ] ) ; resolve ( { bids , asks , } ) ; } ) ; _ORDERBOOK_CACHE . set ( market . toString ( ) , orderbook ) ; return orderbook ; } , [ refresh , swapClient . program . provider . connection , market , marketClient ] ) ; useEffect ( ( ) => { let listener ; if ( marketClient ?. bidsAddress ) { listener = swapClient . program . provider . connection . onAccountChange ( marketClient ?. bidsAddress , async ( info ) => { const bids = OrderbookSide . decode ( marketClient , info . data ) ; const orderbook = await _ORDERBOOK_CACHE . get ( marketClient . address . toString ( ) ) ; const oldBestBid = orderbook ?. bids . items ( true ) . next ( ) . value ; const newBestBid = bids . items ( true ) . next ( ) . value ; if ( orderbook && oldBestBid && newBestBid && oldBestBid . price !== newBestBid . price ) { orderbook . bids = bids ; setRefresh ( ( r ) => r + 1 ) ; } } ) ; } return ( ) => { if ( listener ) { swapClient . program . provider . connection . removeAccountChangeListener ( listener ) ; } } ; } , [ marketClient , marketClient ?. bidsAddress , swapClient . program . provider . connection , ] ) ; useEffect ( ( ) => { let listener ; if ( marketClient ?. asksAddress ) { listener = swapClient . program . provider . connection . onAccountChange ( marketClient ?. asksAddress , async ( info ) => { const asks = OrderbookSide . decode ( marketClient , info . data ) ; const orderbook = await _ORDERBOOK_CACHE . get ( marketClient . address . toString ( ) ) ; const oldBestOffer = orderbook ?. asks . items ( false ) . next ( ) . value ; const newBestOffer = asks . items ( false ) . next ( ) . value ; if ( orderbook && oldBestOffer && newBestOffer && oldBestOffer . price !== newBestOffer . price ) { orderbook . asks = asks ; setRefresh ( ( r ) => r + 1 ) ; } } ) ; } return ( ) => { if ( listener ) { swapClient . program . provider . connection . removeAccountChangeListener ( listener ) ; } } ; } , [ marketClient , marketClient ?. bidsAddress , swapClient . program . provider . connection , ] ) ; if ( asyncOrderbook . result ) { return asyncOrderbook . result ; } return undefined ; } export function useMarketName ( market ) { const tokenMap = useTokenMap ( ) ; const marketClient = useMarket ( market ) ; if ( ! marketClient ) { return null ; } const baseTicker = marketClient ? tokenMap . get ( marketClient ?. baseMintAddress . toString ( ) ) ?. symbol : "-" ; const quoteTicker = marketClient ? tokenMap . get ( marketClient ?. quoteMintAddress . toString ( ) ) ?. symbol : "-" ; const name = `${ baseTicker } / ${ quoteTicker }` ; return name ; } export function useBbo ( market ? ) { const orderbook = useOrderbook ( market ) ; if ( orderbook === undefined ) { return undefined ; } const bestBid = orderbook . bids . items ( true ) . next ( ) . value ; const bestOffer = orderbook . asks . items ( false ) . next ( ) . value ; if ( ! bestBid && ! bestOffer ) { return { } ; } if ( ! bestBid ) { return { bestOffer : bestOffer . price } ; } if ( ! bestOffer ) { return { bestBid : bestBid . price } ; } const mid = ( bestBid . price + bestOffer . price ) / 2.0 ; return { bestBid : bestBid . price , bestOffer : bestOffer . price , mid } ; } export function useFairRoute ( fromMint , toMint ) { const route = useRoute ( fromMint , toMint ) ; const fromBbo = useBbo ( route ? route [ 0 ] : undefined ) ; const fromMarket = useMarket ( route ? route [ 0 ] : undefined ) ; const toBbo = useBbo ( route ? route [ 1 ] : undefined ) ; if ( route === null ) { return undefined ; } if ( route . length === 1 && fromBbo !== undefined ) { if ( fromMarket === undefined ) { return undefined ; } if ( fromMarket ?. baseMintAddress . equals ( fromMint ) || ( fromMarket ?. baseMintAddress . equals ( WRAPPED_SOL_MINT ) && fromMint . equals ( SOL_MINT ) ) ) { return fromBbo . bestBid && 1.0 / fromBbo . bestBid ; } else { return fromBbo . bestOffer && fromBbo . bestOffer ; } } if ( fromBbo === undefined || fromBbo . bestBid === undefined || toBbo === undefined || toBbo . bestOffer === undefined ) { return undefined ; } return toBbo . bestOffer / fromBbo . bestBid ; } export function useRoute ( fromMint , toMint ) { const route = useRouteVerbose ( fromMint , toMint ) ; if ( route === null ) { return null ; } return route . markets ; } export function useRouteVerbose ( fromMint , toMint ) { const { swapClient } = useDexContext ( ) ; const { wormholeMap , solletMap } = useTokenListContext ( ) ; const asyncRoute = useAsync ( async ( ) => { const swapMarket = await wormholeSwapMarket ( swapClient . program . provider . connection , fromMint , toMint , wormholeMap , solletMap ) ; if ( swapMarket !== null ) { const [ wormholeMarket , kind ] = swapMarket ; return { markets : [ wormholeMarket ] , kind } ; } const markets = swapClient . route ( fromMint . equals ( SOL_MINT ) ? WRAPPED_SOL_MINT : fromMint , toMint . equals ( SOL_MINT ) ? WRAPPED_SOL_MINT : toMint ) ; if ( markets === null ) { return null ; } const kind = "usdx" ; return { markets , kind } ; } , [ fromMint , toMint , swapClient ] ) ; if ( asyncRoute . result ) { return asyncRoute . result ; } return null ; } type Orderbook = { bids : OrderbookSide ; asks : OrderbookSide ; } ; type RouteKind = "wormhole-native" | "wormhole-sollet" | "usdx" ; const WORMHOLE_NATIVE_MAP = new Map < string , PublicKey > ( [ [ wormKey ( WORM_USDC_MINT , USDC_MINT ) , WORM_USDC_MARKET ] , [ wormKey ( WORM_USDT_MINT , USDT_MINT ) , WORM_USDT_MARKET ] , ] ) ; function wormKey ( fromMint , toMint ) { const [ first , second ] = fromMint < toMint ? [ fromMint , toMint ] : [ toMint , fromMint ] ; return first . toString ( ) + second . toString ( ) ; } async function wormholeSwapMarket ( conn , fromMint , toMint , wormholeMap , solletMap ) { let market = wormholeNativeMarket ( fromMint , toMint ) ; if ( market !== null ) { return [ market , "wormhole-native" ] ; } market = await wormholeSolletMarket ( conn , fromMint , toMint , wormholeMap , solletMap ) ; if ( market === null ) { return null ; } return [ market , "wormhole-sollet" ] ; } function wormholeNativeMarket ( fromMint , toMint ) { return WORMHOLE_NATIVE_MAP . get ( wormKey ( fromMint , toMint ) ) ?? null ; } async function wormholeSolletMarket ( conn , fromMint , toMint , wormholeMap , solletMap ) { const fromWormhole = wormholeMap . get ( fromMint . toString ( ) ) ; const isFromWormhole = fromWormhole !== undefined ; const toWormhole = wormholeMap . get ( toMint . toString ( ) ) ; const isToWormhole = toWormhole !== undefined ; const fromSollet = solletMap . get ( fromMint . toString ( ) ) ; const isFromSollet = fromSollet !== undefined ; const toSollet = solletMap . get ( toMint . toString ( ) ) ; const isToSollet = toSollet !== undefined ; if ( ( isFromWormhole || isToWormhole ) && isFromWormhole !== isToWormhole ) { if ( ( isFromSollet || isToSollet ) && isFromSollet !== isToSollet ) { const base = isFromSollet ? fromMint : toMint ; const [ quote , wormholeInfo ] = isFromWormhole ? [ fromMint , fromWormhole ] : [ toMint , toWormhole ] ; const solletInfo = await fetchSolletInfo ( base ) ; if ( solletInfo . erc20Contract !== wormholeInfo ! . extensions ?. address ) { return null ; } const market = await deriveWormholeMarket ( base , quote ) ; if ( market === null ) { return null ; } const marketExists = await requestWormholeSwapMarketIfNeeded ( conn , base , quote , market , solletInfo ) ; if ( ! marketExists ) { return null ; } return market ; } } return null ; } async function deriveWormholeMarket ( baseMint , quoteMint , version = 0 ) { if ( version > 99 ) { console . log ( "Swap market version cannot be greater than 99" ) ; return null ; } if ( version < 0 ) { console . log ( "Version cannot be less than zero" ) ; return null ; } const padToTwo = ( n ) => ( n <= 99 ? `0${ n }` . slice ( - 2 ) : n ) ; const seed = baseMint . toString ( ) . slice ( 0 , 15 ) + quoteMint . toString ( ) . slice ( 0 , 15 ) + padToTwo ( version ) ; return await PublicKey . createWithSeed ( WORM_MARKET_BASE , seed , DEX_PID ) ; } type Bbo = { bestBid ? : number ; bestOffer ? : number ; mid ? : number ; } ; const _ORDERBOOK_CACHE = new Map < string , Promise < Orderbook > > ( ) ; const _MARKET_CACHE = new Map < string , Promise < Market > > ( ) ;