Wormhole swap market routes
This commit is contained in:
parent
726a832524
commit
6aa831d298
|
@ -152,7 +152,7 @@ class NotifyingProvider extends Provider {
|
|||
return txSig;
|
||||
} catch (err) {
|
||||
this.onTransaction(undefined, err);
|
||||
throw err;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { PublicKey } from "@solana/web3.js";
|
|||
import { useTokenMap } from "./context/TokenList";
|
||||
import { useSwapContext, useSwapFair } from "./context/Swap";
|
||||
import { useMint } from "./context/Mint";
|
||||
import { useDexContext, useMarketName, useFair } from "./context/Dex";
|
||||
import { useRoute, useMarketName, useFair } from "./context/Dex";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
infoLabel: {
|
||||
|
@ -100,7 +100,7 @@ function InfoButton() {
|
|||
|
||||
function InfoDetails() {
|
||||
const { fromMint, toMint } = useSwapContext();
|
||||
const { swapClient } = useDexContext();
|
||||
const route = useRoute(fromMint, toMint);
|
||||
const tokenMap = useTokenMap();
|
||||
const fromMintTicker = tokenMap.get(fromMint.toString())?.symbol;
|
||||
const toMintTicker = tokenMap.get(toMint.toString())?.symbol;
|
||||
|
@ -108,7 +108,7 @@ function InfoDetails() {
|
|||
{ ticker: fromMintTicker, mint: fromMint },
|
||||
{ ticker: toMintTicker, mint: toMint },
|
||||
];
|
||||
const route = swapClient.route(fromMint, toMint);
|
||||
|
||||
return (
|
||||
<div style={{ padding: "15px", width: "250px" }}>
|
||||
<div>
|
||||
|
|
|
@ -20,6 +20,7 @@ import { useSwappableTokens } from "./context/TokenList";
|
|||
const useStyles = makeStyles(() => ({
|
||||
dialogContent: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
textField: {
|
||||
width: "100%",
|
||||
|
@ -98,7 +99,6 @@ export default function TokenDialog({
|
|||
onClick={(mint) => {
|
||||
setMint(mint);
|
||||
onClose();
|
||||
setTokenFilter("");
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import React, { useContext, useState, useEffect, useMemo } from "react";
|
||||
import React, { useContext, useState, useEffect } from "react";
|
||||
import { useAsync } from "react-async-hook";
|
||||
import { TokenInfo } from "@solana/spl-token-registry";
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
import * as anchor from "@project-serum/anchor";
|
||||
import { Swap as SwapClient } from "@project-serum/swap";
|
||||
import {
|
||||
|
@ -7,9 +9,18 @@ import {
|
|||
OpenOrders,
|
||||
Orderbook as OrderbookSide,
|
||||
} from "@project-serum/serum";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { DEX_PID } from "../../utils/pubkeys";
|
||||
import { useTokenMap } from "./TokenList";
|
||||
import {
|
||||
DEX_PID,
|
||||
USDC_MINT,
|
||||
USDT_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";
|
||||
|
||||
type DexContext = {
|
||||
// Maps market address to open orders accounts.
|
||||
|
@ -195,9 +206,12 @@ export function useOrderbook(market?: PublicKey): Orderbook | undefined {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export function useMarketName(market: PublicKey): string {
|
||||
export function useMarketName(market: PublicKey): string | null {
|
||||
const tokenMap = useTokenMap();
|
||||
const marketClient = useMarket(market);
|
||||
if (!marketClient) {
|
||||
return null;
|
||||
}
|
||||
const baseTicker = marketClient
|
||||
? tokenMap.get(marketClient?.baseMintAddress.toString())?.symbol
|
||||
: "-";
|
||||
|
@ -216,6 +230,12 @@ export function useFair(market?: PublicKey): number | undefined {
|
|||
}
|
||||
const bestBid = orderbook.bids.items(true).next().value;
|
||||
const bestOffer = orderbook.asks.items(false).next().value;
|
||||
if (!bestBid) {
|
||||
return bestOffer.price;
|
||||
}
|
||||
if (!bestOffer) {
|
||||
return bestBid.price;
|
||||
}
|
||||
const mid = (bestBid.price + bestOffer.price) / 2.0;
|
||||
return mid;
|
||||
}
|
||||
|
@ -252,18 +272,166 @@ export function useFairRoute(
|
|||
return toFair / fromFair;
|
||||
}
|
||||
|
||||
// Types of routes.
|
||||
//
|
||||
// 1. Direct trades on USDC quoted markets.
|
||||
// 2. Transitive trades across two USDC qutoed markets.
|
||||
// 3. Wormhole <-> Sollet one-to-one swap markets.
|
||||
// 4. Wormhole <-> Native one-to-one swap markets.
|
||||
//
|
||||
export function useRoute(
|
||||
fromMint: PublicKey,
|
||||
toMint: PublicKey
|
||||
): Array<PublicKey> | null {
|
||||
const { swapClient } = useDexContext();
|
||||
return useMemo(
|
||||
() => swapClient.route(fromMint, toMint),
|
||||
[swapClient, fromMint, toMint]
|
||||
const { wormholeMap, solletMap } = useTokenListContext();
|
||||
const asyncRoute = useAsync(async () => {
|
||||
const wormholeMarket = await wormholeSwapMarket(
|
||||
swapClient.program.provider.connection,
|
||||
fromMint,
|
||||
toMint,
|
||||
wormholeMap,
|
||||
solletMap
|
||||
);
|
||||
if (wormholeMarket !== null) {
|
||||
return [wormholeMarket];
|
||||
}
|
||||
return swapClient.route(fromMint, toMint);
|
||||
}, [fromMint, toMint, swapClient]);
|
||||
|
||||
if (asyncRoute.result) {
|
||||
return asyncRoute.result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type Orderbook = {
|
||||
bids: OrderbookSide;
|
||||
asks: OrderbookSide;
|
||||
};
|
||||
|
||||
// Wormhole utils.
|
||||
|
||||
// Maps fromMint || toMint (in sort order) to swap market public key.
|
||||
// All markets for wormhole<->native tokens should be here, e.g.
|
||||
// USDC <-> wUSDC.
|
||||
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: PublicKey, toMint: PublicKey): string {
|
||||
const [first, second] =
|
||||
fromMint < toMint ? [fromMint, toMint] : [toMint, fromMint];
|
||||
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<string, TokenInfo>,
|
||||
solletMap: Map<string, TokenInfo>
|
||||
): Promise<PublicKey | null> {
|
||||
let market = wormholeNativeMarket(fromMint, toMint);
|
||||
if (market !== undefined) {
|
||||
return market;
|
||||
}
|
||||
return await wormholeSolletMarket(
|
||||
conn,
|
||||
fromMint,
|
||||
toMint,
|
||||
wormholeMap,
|
||||
solletMap
|
||||
);
|
||||
}
|
||||
|
||||
// Returns the market address of the 1-1 sollet<->wormhole swap market if it
|
||||
// exists. Otherwise, returns null.
|
||||
//
|
||||
// TODO: swap transactions dont work for wormhole yet, since the client
|
||||
// doesnt do any wormhole checks.
|
||||
async function wormholeSolletMarket(
|
||||
conn: Connection,
|
||||
fromMint: PublicKey,
|
||||
toMint: PublicKey,
|
||||
wormholeMap: Map<string, TokenInfo>,
|
||||
solletMap: Map<string, TokenInfo>
|
||||
): Promise<PublicKey | null> {
|
||||
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;
|
||||
}
|
||||
|
||||
// Calculates the deterministic address for the sollet<->wormhole 1-1 swap
|
||||
// market.
|
||||
async function deriveWormholeMarket(
|
||||
baseMint: PublicKey,
|
||||
quoteMint: PublicKey,
|
||||
version = 0
|
||||
): Promise<PublicKey | null> {
|
||||
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: number) => (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);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,100 @@
|
|||
import { useAsync, UseAsyncReturn } from "react-async-hook";
|
||||
import { Connection, PublicKey } from "@solana/web3.js";
|
||||
|
||||
// Token info tracked by the sollet bridge.
|
||||
type SolletInfo = {
|
||||
blockchain: string;
|
||||
erc20Contract: string;
|
||||
name: string;
|
||||
splMint: PublicKey;
|
||||
ticker: string;
|
||||
};
|
||||
|
||||
export function useSolletInfo(mint: PublicKey): UseAsyncReturn<SolletInfo> {
|
||||
return useAsync(async () => {
|
||||
return fetchSolletInfo(mint);
|
||||
}, [mint]);
|
||||
}
|
||||
|
||||
// Fetches the token info from the sollet bridge.
|
||||
export async function fetchSolletInfo(mint: PublicKey): Promise<SolletInfo> {
|
||||
let info = _SOLLET_INFO_CACHE.get(mint.toString());
|
||||
if (info !== undefined) {
|
||||
return info;
|
||||
}
|
||||
|
||||
const infoRaw = await swapApiRequest("GET", `coins/sol/${mint.toString()}`);
|
||||
info = { ...infoRaw, splMint: new PublicKey(infoRaw.splMint) };
|
||||
_SOLLET_INFO_CACHE.set(mint.toString(), info!);
|
||||
|
||||
return info!;
|
||||
}
|
||||
|
||||
// Requests the creation of a sollet wormhole swap market, if it doesn't
|
||||
// already exist. Note: this triggers a creation notification. Creation
|
||||
// doesn't happen immediately, but at some unspecified point in the future
|
||||
// since market makers need to setup on the swap market and provide liquidity.
|
||||
//
|
||||
// Returns true if the market exists already. False otherwise.
|
||||
export async function requestWormholeSwapMarketIfNeeded(
|
||||
connection: Connection,
|
||||
solletMint: PublicKey,
|
||||
wormholeMint: PublicKey,
|
||||
swapMarket: PublicKey,
|
||||
solletInfo: SolletInfo
|
||||
): Promise<boolean> {
|
||||
const cached = _SWAP_MARKET_EXISTS_CACHE.get(swapMarket.toString());
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
const acc = await connection.getAccountInfo(swapMarket);
|
||||
if (acc === null) {
|
||||
_SWAP_MARKET_EXISTS_CACHE.set(swapMarket.toString(), false);
|
||||
const resource = `wormhole/pool/${
|
||||
solletInfo.ticker
|
||||
}/${swapMarket.toString()}/${solletMint.toString()}/${wormholeMint.toString()}`;
|
||||
swapApiRequest("POST", resource).catch(console.error);
|
||||
return false;
|
||||
} else {
|
||||
_SWAP_MARKET_EXISTS_CACHE.set(swapMarket.toString(), true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export async function swapApiRequest(
|
||||
method: string,
|
||||
path: string,
|
||||
body?: Object
|
||||
) {
|
||||
let headers: any = {};
|
||||
let params: any = { headers, method };
|
||||
if (method === "GET") {
|
||||
params.cache = "no-cache";
|
||||
} else if (body) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
params.body = JSON.stringify(body);
|
||||
}
|
||||
let resp = await fetch(`https://swap.sollet.io/api/${path}`, params);
|
||||
return await handleSwapApiResponse(resp);
|
||||
}
|
||||
|
||||
async function handleSwapApiResponse(resp: Response) {
|
||||
let json = await resp.json();
|
||||
if (!json.success) {
|
||||
throw new SwapApiError(json.error, resp.status);
|
||||
}
|
||||
return json.result;
|
||||
}
|
||||
|
||||
export class SwapApiError extends Error {
|
||||
readonly name: string;
|
||||
readonly status: number;
|
||||
constructor(msg: string, status: number) {
|
||||
super(msg);
|
||||
this.name = "SwapApiError";
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
|
||||
const _SOLLET_INFO_CACHE = new Map<string, SolletInfo>();
|
||||
const _SWAP_MARKET_EXISTS_CACHE = new Map<string, boolean>();
|
|
@ -4,6 +4,8 @@ import { USDC_MINT, USDT_MINT } from "../../utils/pubkeys";
|
|||
|
||||
type TokenListContext = {
|
||||
tokenMap: Map<string, TokenInfo>;
|
||||
wormholeMap: Map<string, TokenInfo>;
|
||||
solletMap: Map<string, TokenInfo>;
|
||||
swappableTokens: TokenInfo[];
|
||||
swappableTokensSollet: TokenInfo[];
|
||||
swappableTokensWormhole: TokenInfo[];
|
||||
|
@ -15,6 +17,8 @@ export function TokenListContextProvider(props: any) {
|
|||
() => props.tokenList.filterByClusterSlug("mainnet-beta").getList(),
|
||||
[props.tokenList]
|
||||
);
|
||||
|
||||
// Token map for quick lookup.
|
||||
const tokenMap = useMemo(() => {
|
||||
const tokenMap = new Map();
|
||||
tokenList.forEach((t: TokenInfo) => {
|
||||
|
@ -22,6 +26,8 @@ export function TokenListContextProvider(props: any) {
|
|||
});
|
||||
return tokenMap;
|
||||
}, [tokenList]);
|
||||
|
||||
// Tokens with USD(x) quoted markets.
|
||||
const swappableTokens = useMemo(() => {
|
||||
const tokens = tokenList
|
||||
.filter((t: TokenInfo) => {
|
||||
|
@ -40,7 +46,9 @@ export function TokenListContextProvider(props: any) {
|
|||
);
|
||||
return tokens;
|
||||
}, [tokenList, tokenMap]);
|
||||
const swappableTokensSollet = useMemo(() => {
|
||||
|
||||
// Sollet wrapped tokens.
|
||||
const [swappableTokensSollet, solletMap] = useMemo(() => {
|
||||
const tokens = tokenList.filter((t: TokenInfo) => {
|
||||
const isSollet = t.tags?.includes("wrapped-sollet");
|
||||
return isSollet;
|
||||
|
@ -48,9 +56,14 @@ export function TokenListContextProvider(props: any) {
|
|||
tokens.sort((a: TokenInfo, b: TokenInfo) =>
|
||||
a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0
|
||||
);
|
||||
return tokens;
|
||||
return [
|
||||
tokens,
|
||||
new Map<string, TokenInfo>(tokens.map((t: TokenInfo) => [t.address, t])),
|
||||
];
|
||||
}, [tokenList]);
|
||||
const swappableTokensWormhole = useMemo(() => {
|
||||
|
||||
// Wormhole wrapped tokens.
|
||||
const [swappableTokensWormhole, wormholeMap] = useMemo(() => {
|
||||
const tokens = tokenList.filter((t: TokenInfo) => {
|
||||
const isSollet = t.tags?.includes("wormhole");
|
||||
return isSollet;
|
||||
|
@ -58,13 +71,18 @@ export function TokenListContextProvider(props: any) {
|
|||
tokens.sort((a: TokenInfo, b: TokenInfo) =>
|
||||
a.symbol < b.symbol ? -1 : a.symbol > b.symbol ? 1 : 0
|
||||
);
|
||||
return tokens;
|
||||
return [
|
||||
tokens,
|
||||
new Map<string, TokenInfo>(tokens.map((t: TokenInfo) => [t.address, t])),
|
||||
];
|
||||
}, [tokenList]);
|
||||
|
||||
return (
|
||||
<_TokenListContext.Provider
|
||||
value={{
|
||||
tokenMap,
|
||||
wormholeMap,
|
||||
solletMap,
|
||||
swappableTokens,
|
||||
swappableTokensWormhole,
|
||||
swappableTokensSollet,
|
||||
|
@ -75,7 +93,7 @@ export function TokenListContextProvider(props: any) {
|
|||
);
|
||||
}
|
||||
|
||||
function useTokenListContext(): TokenListContext {
|
||||
export function useTokenListContext(): TokenListContext {
|
||||
const ctx = useContext(_TokenListContext);
|
||||
if (ctx === null) {
|
||||
throw new Error("Context not available");
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { PublicKey } from "@solana/web3.js";
|
||||
|
||||
export const DEX_PID = new PublicKey(
|
||||
"9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
|
||||
);
|
||||
|
||||
export const SRM_MINT = new PublicKey(
|
||||
"SRMuApVNdxXokk5GT7XD5cUUgXMBCoAz2LHeuAoKWRt"
|
||||
);
|
||||
|
@ -12,6 +16,22 @@ export const USDT_MINT = new PublicKey(
|
|||
"Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"
|
||||
);
|
||||
|
||||
export const DEX_PID = new PublicKey(
|
||||
"9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin"
|
||||
export const WORM_MARKET_BASE = new PublicKey(
|
||||
"6a9wpsZpZGxGhFVSQBpcTNjNjytdbSA1iUw1A5KNDxPw"
|
||||
);
|
||||
|
||||
export const WORM_USDC_MINT = new PublicKey(
|
||||
"FVsXUnbhifqJ4LiXQEbpUtXVdB8T5ADLKqSs5t1oc54F"
|
||||
);
|
||||
|
||||
export const WORM_USDC_MARKET = new PublicKey(
|
||||
"6nGMps9VfDjkKEwYjdSNqN1ToXkLae4VsN49fzBiDFBd"
|
||||
);
|
||||
|
||||
export const WORM_USDT_MINT = new PublicKey(
|
||||
"9w97GdWUYYaamGwdKMKZgGzPduZJkiFizq4rz5CPXRv2"
|
||||
);
|
||||
|
||||
export const WORM_USDT_MARKET = new PublicKey(
|
||||
"4v6e6vNXAaEunrvbqkYnKwbaWfck8a2KVR4uRAVXxVwC"
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue