diff --git a/package.json b/package.json index dab0561..03de14c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@project-serum/anchor": "^0.5.1-beta.2", "@project-serum/serum": "^0.13.34", "@project-serum/sol-wallet-adapter": "^0.2.0", - "@project-serum/swap": "^0.1.0-alpha.5", + "@project-serum/swap": "^0.1.0-alpha.7", "@solana/spl-token": "^0.1.4", "@solana/spl-token-registry": "^0.2.86", "@solana/web3.js": "^1.10.1", diff --git a/src/swap/components/Swap.tsx b/src/swap/components/Swap.tsx index 504c552..ac4480d 100644 --- a/src/swap/components/Swap.tsx +++ b/src/swap/components/Swap.tsx @@ -17,11 +17,17 @@ import { DexContextProvider, useDexContext, useOpenOrders, - useRoute, + useRouteVerbose, useMarket, } from "./context/Dex"; import { MintContextProvider, useMint } from "./context/Mint"; -import { TokenListContextProvider, useTokenMap } from "./context/TokenList"; +import { + TokenListContextProvider, + useTokenMap, + useTokenListContext, + SPL_REGISTRY_SOLLET_TAG, + SPL_REGISTRY_WORM_TAG, +} from "./context/TokenList"; import { TokenContextProvider, useOwnedTokenAccount } from "./context/Token"; import TokenDialog from "./TokenDialog"; import { SettingsButton } from "./Settings"; @@ -59,10 +65,18 @@ export default function Swap({ style, provider, tokenList, + fromMint, + toMint, + fromAmount, + toAmount, }: { - style?: any; provider: Provider; tokenList: TokenListContainer; + fromMint?: PublicKey; + toMint?: PublicKey; + fromAmount?: number; + toAmount?: number; + style?: any; }) { const swapClient = new SwapClient(provider, tokenList); return ( @@ -70,7 +84,12 @@ export default function Swap({ - + @@ -269,16 +288,48 @@ function SwapButton() { const fromMintInfo = useMint(fromMint); const toMintInfo = useMint(toMint); const openOrders = useOpenOrders(); - const route = useRoute(fromMint, toMint); - const fromMarket = useMarket(route ? route[0] : undefined); - const toMarket = useMarket(route ? route[1] : undefined); + const route = useRouteVerbose(fromMint, toMint); + const fromMarket = useMarket( + route && route.markets ? route.markets[0] : undefined + ); + const toMarket = useMarket( + route && route.markets ? route.markets[1] : undefined + ); + const { wormholeMap, solletMap } = useTokenListContext(); + + // True iff the button should be activated. + const enabled = + // 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); const sendSwapTransaction = async () => { if (!fromMintInfo || !toMintInfo) { throw new Error("Unable to calculate mint decimals"); } - const amount = new BN(fromAmount).muln(10 ** fromMintInfo.decimals); - const minExpectedSwapAmount = new BN(toAmount * 10 ** toMintInfo.decimals) + const amount = new BN(fromAmount).mul( + new BN(10).pow(new BN(fromMintInfo.decimals)) + ); + const minExpectedSwapAmount = new BN(toAmount) + .mul(new BN(10).pow(new BN(toMintInfo.decimals))) .muln(100 - slippage) .divn(100); const fromOpenOrders = fromMarket @@ -307,9 +358,7 @@ function SwapButton() { variant="contained" className={styles.swapButton} onClick={sendSwapTransaction} - disabled={ - swapClient.program.provider.wallet.publicKey === null || route === null - } + disabled={!enabled} > Swap diff --git a/src/swap/components/context/Dex.tsx b/src/swap/components/context/Dex.tsx index 85d3220..9f31c9e 100644 --- a/src/swap/components/context/Dex.tsx +++ b/src/swap/components/context/Dex.tsx @@ -272,6 +272,17 @@ export function useFairRoute( return toFair / fromFair; } +export function useRoute( + fromMint: PublicKey, + toMint: PublicKey +): Array | null { + const route = useRouteVerbose(fromMint, toMint); + if (route === null) { + return null; + } + return route.markets; +} + // Types of routes. // // 1. Direct trades on USDC quoted markets. @@ -279,24 +290,30 @@ export function useFairRoute( // 3. Wormhole <-> Sollet one-to-one swap markets. // 4. Wormhole <-> Native one-to-one swap markets. // -export function useRoute( +export function useRouteVerbose( fromMint: PublicKey, toMint: PublicKey -): Array | null { +): { markets: Array; kind: RouteKind } | null { const { swapClient } = useDexContext(); const { wormholeMap, solletMap } = useTokenListContext(); const asyncRoute = useAsync(async () => { - const wormholeMarket = await wormholeSwapMarket( + const swapMarket = await wormholeSwapMarket( swapClient.program.provider.connection, fromMint, toMint, wormholeMap, solletMap ); - if (wormholeMarket !== null) { - return [wormholeMarket]; + if (swapMarket !== null) { + const [wormholeMarket, kind] = swapMarket; + return { markets: [wormholeMarket], kind }; } - return swapClient.route(fromMint, toMint); + const markets = swapClient.route(fromMint, toMint); + if (markets === null) { + return null; + } + const kind: RouteKind = "usdx"; + return { markets, kind }; }, [fromMint, toMint, swapClient]); if (asyncRoute.result) { @@ -312,6 +329,8 @@ type Orderbook = { // Wormhole utils. +type RouteKind = "wormhole-native" | "wormhole-sollet" | "usdx"; + // Maps fromMint || toMint (in sort order) to swap market public key. // All markets for wormhole<->native tokens should be here, e.g. // USDC <-> wUSDC. @@ -326,31 +345,35 @@ function wormKey(fromMint: PublicKey, toMint: PublicKey): string { return first.toString() + second.toString(); } -function wormholeNativeMarket( - fromMint: PublicKey, - toMint: PublicKey -): PublicKey | undefined { - return WORMHOLE_NATIVE_MAP.get(wormKey(fromMint, toMint)); -} - async function wormholeSwapMarket( conn: Connection, fromMint: PublicKey, toMint: PublicKey, wormholeMap: Map, solletMap: Map -): Promise { +): Promise<[PublicKey, RouteKind] | null> { let market = wormholeNativeMarket(fromMint, toMint); - if (market !== undefined) { - return market; + if (market !== null) { + return [market, "wormhole-native"]; } - return await wormholeSolletMarket( + market = await wormholeSolletMarket( conn, fromMint, toMint, wormholeMap, solletMap ); + if (market === null) { + return null; + } + return [market, "wormhole-sollet"]; +} + +function wormholeNativeMarket( + fromMint: PublicKey, + toMint: PublicKey +): PublicKey | null { + return WORMHOLE_NATIVE_MAP.get(wormKey(fromMint, toMint)) ?? null; } // Returns the market address of the 1-1 sollet<->wormhole swap market if it diff --git a/src/swap/components/context/Swap.tsx b/src/swap/components/context/Swap.tsx index 7af7b51..e749e7a 100644 --- a/src/swap/components/context/Swap.tsx +++ b/src/swap/components/context/Swap.tsx @@ -45,10 +45,10 @@ export type SwapContext = { const _SwapContext = React.createContext(null); export function SwapContextProvider(props: any) { - const [fromMint, setFromMint] = useState(SRM_MINT); - const [toMint, setToMint] = useState(USDC_MINT); - const [fromAmount, _setFromAmount] = useState(0); - const [toAmount, _setToAmount] = useState(0); + 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); // Percent units. const [slippage, setSlippage] = useState(DEFAULT_SLIPPAGE_PERCENT); diff --git a/src/swap/components/context/TokenList.tsx b/src/swap/components/context/TokenList.tsx index 7bdfefd..0134751 100644 --- a/src/swap/components/context/TokenList.tsx +++ b/src/swap/components/context/TokenList.tsx @@ -12,6 +12,12 @@ type TokenListContext = { }; const _TokenListContext = React.createContext(null); +// Tag in the spl-token-registry for sollet wrapped tokens. +export const SPL_REGISTRY_SOLLET_TAG = "wrapped-sollet"; + +// Tag in the spl-token-registry for wormhole wrapped tokens. +export const SPL_REGISTRY_WORM_TAG = "wormhole"; + export function TokenListContextProvider(props: any) { const tokenList = useMemo( () => props.tokenList.filterByClusterSlug("mainnet-beta").getList(), @@ -50,7 +56,7 @@ export function TokenListContextProvider(props: any) { // Sollet wrapped tokens. const [swappableTokensSollet, solletMap] = useMemo(() => { const tokens = tokenList.filter((t: TokenInfo) => { - const isSollet = t.tags?.includes("wrapped-sollet"); + const isSollet = t.tags?.includes(SPL_REGISTRY_SOLLET_TAG); return isSollet; }); tokens.sort((a: TokenInfo, b: TokenInfo) => @@ -65,7 +71,7 @@ export function TokenListContextProvider(props: any) { // Wormhole wrapped tokens. const [swappableTokensWormhole, wormholeMap] = useMemo(() => { const tokens = tokenList.filter((t: TokenInfo) => { - const isSollet = t.tags?.includes("wormhole"); + const isSollet = t.tags?.includes(SPL_REGISTRY_WORM_TAG); return isSollet; }); tokens.sort((a: TokenInfo, b: TokenInfo) => diff --git a/yarn.lock b/yarn.lock index 197153c..88791d4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1588,10 +1588,10 @@ bs58 "^4.0.1" eventemitter3 "^4.0.4" -"@project-serum/swap@^0.1.0-alpha.5": - version "0.1.0-alpha.5" - resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.5.tgz#106fdf5354c3c17f1832ab623122739fd45e2e52" - integrity sha512-ZJW9XNlZyhIq/C8pwKhFvf7duKth8dfu7vkgVtfUp281dBgSXx6IwrigpYcJ9x/VZMr9LMrkUVW1LiXd8XZdEQ== +"@project-serum/swap@^0.1.0-alpha.7": + version "0.1.0-alpha.7" + resolved "https://registry.yarnpkg.com/@project-serum/swap/-/swap-0.1.0-alpha.7.tgz#82bdd06e57814b9a42cf127c26b53bfc3b48438b" + integrity sha512-oZU9bA0znbIcxCKM1sxjOjxHCV1n5iPNowbYhtmsbhH6DczgjIsPO3gGJ00TJOBPkOn7gdtynpDAhGar3tSBPw== dependencies: "@project-serum/anchor" "^0.5.1-beta.2" "@project-serum/serum" "^0.13.34"