Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/sc 2663/wrap amm quote requests in api #27

Merged
merged 3 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions packages/api/src/prices/fetchEthPrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import fetchSpotPrice from './fetchSpotPrice';

/** Returns the current price of ETH in USDC terms */
const fetchEthPrice = async ({ network }: { network?: number }) => {
const { price } = await fetchSpotPrice({
network,
tokenAddress: 'ETH',
quoteToken: 'USDC',
});

return price;
};

export default fetchEthPrice;
22 changes: 22 additions & 0 deletions packages/api/src/prices/fetchPrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { MarketplacePrice } from '@nftx/types';
import fetchQuote, {
BuyArgs,
MintArgs,
RedeemArgs,
SellArgs,
TokenArgs,
SwapArgs,
} from './fetchQuote';

/** Returns an off-chain price for a transaction */
function fetchPrice(args: BuyArgs): Promise<MarketplacePrice>;
function fetchPrice(args: SellArgs): Promise<MarketplacePrice>;
function fetchPrice(args: SwapArgs): Promise<MarketplacePrice>;
function fetchPrice(args: MintArgs): Promise<MarketplacePrice>;
function fetchPrice(args: RedeemArgs): Promise<MarketplacePrice>;
function fetchPrice(args: TokenArgs): Promise<MarketplacePrice>;
function fetchPrice(args: any) {
return fetchQuote({ ...args, quoteType: 'price' });
}

export default fetchPrice;
156 changes: 104 additions & 52 deletions packages/api/src/prices/fetchQuote.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,134 @@
import type {
Address,
BigIntish,
MarketplacePrice,
MarketplaceQuote,
Permit2Quote,
TokenIds,
QuoteToken,
} from '@nftx/types';
import { queryApi } from '../utils';
import config from '@nftx/config';
import { getTokenIdAmounts, getUniqueTokenIds } from '@nftx/utils';

type CommonArgs = {
type: 'buy' | 'sell' | 'swap' | 'mint' | 'redeem';
export type BuyArgs = {
network?: number;
type: 'buy';
vaultId: string;
tokenIds: TokenIds;
};
export type SellArgs = {
network?: number;
type: 'sell';
vaultId: string;
buyTokenIds?: TokenIds;
sellTokenIds?: TokenIds;
tokenIds: TokenIds;
};
export type SwapArgs = {
network?: number;
type: 'swap';
vaultId: string;
mintTokenIds: TokenIds;
redeemTokenIds: TokenIds;
};
type PriceArgs = CommonArgs & {
quoteType: 'price';
export type MintArgs = {
network?: number;
type: 'mint';
vaultId: string;
tokenIds: TokenIds;
};
export type RedeemArgs = {
network?: number;
type: 'redeem';
vaultId: string;
tokenIds: TokenIds;
};
type QuoteArgs = CommonArgs & {
quoteType: 'quote';
export type TokenArgs = {
network?: number;
type: 'erc20';
buyToken: QuoteToken;
sellToken: QuoteToken;
buyAmount?: BigIntish;
sellAmount?: BigIntish;
};

type PriceArgs = { quoteType: 'price' };
type QuoteArgs = {
quoteType?: 'quote';
userAddress: Address;
slippagePercentage?: number;
permit2?: Permit2Quote;
};

function fetchQuote(args: PriceArgs): Promise<MarketplacePrice>;
function fetchQuote(args: QuoteArgs): Promise<MarketplaceQuote>;
function fetchQuote(args: PriceArgs | QuoteArgs) {
/** Returns an on-chain quote for a transaction. The response object can be passed into @nftx/trade's fulfill method to execute the quote */
function fetchQuote(args: BuyArgs & PriceArgs): Promise<MarketplacePrice>;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't it easier/more readable just to have an interface with all the props?

Copy link
Collaborator Author

@jackmellis jackmellis May 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did it this way so ts can tell you which props you actually need to provide for each type of txn, otherwise it's a whole mess of optional props and it'd be difficult to know which things you need to pass and when

function fetchQuote(args: BuyArgs & QuoteArgs): Promise<MarketplaceQuote>;
function fetchQuote(args: SellArgs & PriceArgs): Promise<MarketplacePrice>;
function fetchQuote(args: SellArgs & QuoteArgs): Promise<MarketplaceQuote>;
function fetchQuote(args: SwapArgs & PriceArgs): Promise<MarketplacePrice>;
function fetchQuote(args: SwapArgs & QuoteArgs): Promise<MarketplaceQuote>;
function fetchQuote(args: MintArgs & PriceArgs): Promise<MarketplacePrice>;
function fetchQuote(args: MintArgs & QuoteArgs): Promise<MarketplaceQuote>;
function fetchQuote(args: RedeemArgs & PriceArgs): Promise<MarketplacePrice>;
function fetchQuote(args: RedeemArgs & QuoteArgs): Promise<MarketplaceQuote>;
function fetchQuote(args: TokenArgs & PriceArgs): Promise<MarketplacePrice>;
function fetchQuote(args: TokenArgs & QuoteArgs): Promise<MarketplaceQuote>;
function fetchQuote(args: any) {
const {
quoteType,
type,
vaultId,
buyTokenIds: buyTokensAndAmounts,
quoteType = 'quote',
network = config.network,
sellTokenIds: sellTokensAndAmounts,
vaultId,
userAddress,
slippagePercentage,
} = args;

const buyTokenIds = buyTokensAndAmounts
? getUniqueTokenIds(buyTokensAndAmounts)
: undefined;
const buyAmounts = buyTokensAndAmounts
? getTokenIdAmounts(buyTokensAndAmounts)
: undefined;
const sellTokenIds = sellTokensAndAmounts
? getUniqueTokenIds(sellTokensAndAmounts)
: undefined;
const sellAmounts = sellTokensAndAmounts
? getTokenIdAmounts(sellTokensAndAmounts)
: undefined;
const url = `/${network}/quote/${type}`;

if (quoteType === 'price') {
return queryApi<MarketplacePrice>({
url: `/${network}/price`,
query: {
type,
vaultId,
buyTokenIds,
buyAmounts,
sellTokenIds,
sellAmounts,
},
});
const query: Record<string, any> = {
vaultId,
userAddress,
slippagePercentage,
};

switch (type) {
case 'buy':
case 'redeem':
query.buyTokenIds = getUniqueTokenIds(args.tokenIds);
query.buyAmounts = getTokenIdAmounts(args.tokenIds);
break;
case 'sell':
case 'mint':
query.sellTokenIds = getUniqueTokenIds(args.tokenIds);
query.sellAmounts = getTokenIdAmounts(args.tokenIds);
break;
case 'swap':
query.sellTokenIds = getUniqueTokenIds(args.mintTokenIds);
query.sellAmounts = getTokenIdAmounts(args.mintTokenIds);
query.buyTokenIds = getUniqueTokenIds(args.redeemTokenIds);
query.buyAmounts = getTokenIdAmounts(args.redeemTokenIds);
break;
case 'erc20':
query.sellToken = args.sellToken;
query.sellAmount = args.sellAmount;
query.buyToken = args.buyToken;
query.buyAmount = args.buyAmount;
break;
}

const { userAddress, slippagePercentage } = args;
if (args.permit2 && quoteType === 'quote') {
query.permit2 = args.permit2;
}

type QuoteType = typeof quoteType extends 'price'
? MarketplacePrice
: MarketplaceQuote;

const method = quoteType === 'price' ? 'GET' : 'POST';

return queryApi<MarketplaceQuote>({
url: `/${network}/quote`,
query: {
type,
vaultId,
buyTokenIds,
buyAmounts,
sellTokenIds,
sellAmounts,
userAddress,
slippagePercentage,
},
return queryApi<QuoteType>({
url,
query,
method,
});
}

Expand Down
46 changes: 46 additions & 0 deletions packages/api/src/prices/fetchSpotPrice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { QuoteToken } from '@nftx/types';
import fetchPrice from './fetchPrice';
import { WeiPerEther } from '@nftx/constants';
import { UnknownError } from '@nftx/errors';

/** Returns the spot price for buying a token */
const fetchSpotPrice = async ({
tokenAddress,
amount = WeiPerEther,
network,
quoteToken,
}: {
tokenAddress: QuoteToken;
network?: number;
quoteToken?: QuoteToken;
amount?: bigint;
}) => {
// We start by attempting to quote 1 whole token, but if it fails (e.g. due to insufficient liquidity),
// we can try to quote a smaller fraction of the token until we find a price that works.
let fraction = WeiPerEther;
let error: any = new UnknownError('Failed to fetch spot price');

do {
try {
const quote = await fetchPrice({
network,
type: 'erc20',
buyToken: tokenAddress,
sellToken: quoteToken || 'ETH',
buyAmount: fraction,
});

// Extrapolate the price to determine the spot price for 1 whole token, then scale it by the desired amount.
quote.price = quote.vTokenPrice = (quote.price * amount) / fraction;

return quote;
} catch (e) {
error = e;
fraction /= 100n;
}
} while (fraction > 0n);

throw error;
};

export default fetchSpotPrice;
37 changes: 37 additions & 0 deletions packages/api/src/prices/fetchSpread.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { WeiPerEther, Zero } from '@nftx/constants';
import { QuoteToken } from '@nftx/types';
import fetchPrice from './fetchPrice';

/** Returns the difference between a buy and sell of a single token */
const fetchSpread = async ({
tokenAddress,
network,
quoteToken,
}: {
tokenAddress: QuoteToken;
network?: number;
quoteToken?: QuoteToken;
}) => {
try {
const { price: buyPrice } = await fetchPrice({
network,
type: 'erc20',
buyToken: tokenAddress,
sellToken: quoteToken || 'ETH',
buyAmount: WeiPerEther,
});
const { price: sellPrice } = await fetchPrice({
network,
type: 'erc20',
sellToken: tokenAddress,
buyToken: quoteToken || 'ETH',
sellAmount: WeiPerEther,
});

return buyPrice - sellPrice;
} catch {
return Zero;
}
};

export default fetchSpread;
21 changes: 0 additions & 21 deletions packages/api/src/prices/fetchVaultBuyPrice.ts

This file was deleted.

27 changes: 0 additions & 27 deletions packages/api/src/prices/fetchVaultBuyQuote.ts

This file was deleted.

21 changes: 0 additions & 21 deletions packages/api/src/prices/fetchVaultMintPrice.ts

This file was deleted.

Loading
Loading