From aaf1ff2ba8040cfb10b8d8ee2f9a94dc16bd97b7 Mon Sep 17 00:00:00 2001 From: paul launay Date: Tue, 19 Nov 2024 15:01:40 +0100 Subject: [PATCH 1/3] feat(market): fetch custom token price with avnu --- apps/arkmarket/package.json | 1 + .../components/token-actions-price.tsx | 52 ++++++++----------- .../[tokenId]/components/token-actions.tsx | 8 +-- apps/arkmarket/src/constants/tokens.ts | 2 + apps/arkmarket/src/hooks/useUsdPrice.ts | 37 +++++++++++++ apps/arkmarket/src/types/index.ts | 10 +++- pnpm-lock.yaml | 33 ++++++++++-- 7 files changed, 100 insertions(+), 43 deletions(-) create mode 100644 apps/arkmarket/src/hooks/useUsdPrice.ts diff --git a/apps/arkmarket/package.json b/apps/arkmarket/package.json index 5dc66feb..b7c47287 100644 --- a/apps/arkmarket/package.json +++ b/apps/arkmarket/package.json @@ -20,6 +20,7 @@ "@ark-market/ui": "workspace:*", "@ark-project/core": "^2.1.1", "@ark-project/react": "^1.1.1", + "@avnu/avnu-sdk": "^2.1.1", "@hookform/error-message": "^2.0.1", "@starknet-react/chains": "^0.1.7", "@starknet-react/core": "^2.0.0", diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-price.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-price.tsx index f109df16..ae008d94 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-price.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-price.tsx @@ -1,42 +1,32 @@ "use client"; -import { formatEther } from "viem"; +import { formatUnits } from "viem"; import { cn, ellipsableStyles } from "@ark-market/ui"; -import usePrices from "~/hooks/usePrices"; +import type { TokenMarketData } from "~/types"; +import useUsdPrice from "~/hooks/useUsdPrice"; interface TokenActionsPriceProps { - startAmount: string | null; - isListed: boolean; - isAuction: boolean; - hasOffer: boolean; - topOffer: { - amount: string; - order_hash: string; - }; + tokenMarketData: TokenMarketData; } export default function TokenActionsPrice({ - startAmount, - isAuction, - isListed, - topOffer, + tokenMarketData, }: TokenActionsPriceProps) { - const { convertInUsd } = usePrices(); - const amountHex = isListed ? startAmount : topOffer.amount; - const amount = formatEther(BigInt(amountHex ?? 0)); - const amountInUsd = convertInUsd({ amount: BigInt(amountHex ?? 0) }); - - let label = "Best offer"; - - if (isListed) { - if (isAuction) { - label = "Minimum starting price"; - } else { - label = "Current Price"; - } - } + const label = tokenMarketData.is_listed + ? tokenMarketData.listing.is_auction + ? "Minimum starting price" + : "Current Price" + : "Best offer"; + const amount = tokenMarketData.is_listed + ? BigInt(tokenMarketData.listing.start_amount) + : BigInt(tokenMarketData.top_offer.amount); + const currency = tokenMarketData.is_listed + ? tokenMarketData.listing.currency + : tokenMarketData.top_offer.currency; + const usdPrice = useUsdPrice({ amount, currency }); + const amountFormatted = formatUnits(amount, currency.decimals); return (
@@ -48,12 +38,12 @@ export default function TokenActionsPrice({ ellipsableStyles, )} > - {amount} ETH + {amountFormatted} {currency.symbol}
- ${amountInUsd} + ${usdPrice}
-
+
Royalties 5%
diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions.tsx index b21e7c88..6d37d86e 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions.tsx @@ -44,13 +44,7 @@ export default function TokenActions({ isAuction={data.listing.is_auction} expiresAt={data.listing.end_date} /> - + { + const [quote] = await fetchQuotes({ + sellTokenAddress: currency.contract, + buyTokenAddress: USDC, + sellAmount: amount, + }); + + const amountInUsd = quote?.sellAmountInUsd ?? 0; + + return amountInUsd.toLocaleString("en-US", { + minimumFractionDigits: 0, + maximumFractionDigits: 2, + }); + }, + refetchInterval: 30000, + structuralSharing: false, + }); + + return data ?? "0"; +} diff --git a/apps/arkmarket/src/types/index.ts b/apps/arkmarket/src/types/index.ts index 4ca19733..3f1bdf9b 100644 --- a/apps/arkmarket/src/types/index.ts +++ b/apps/arkmarket/src/types/index.ts @@ -254,6 +254,12 @@ export interface PortfolioStats { total_value: string | null; } +export interface Currency { + contract: string; + decimals: number; + symbol: string; +} + export interface TokenMarketData { buy_in_progress: boolean; created_timestamp: number | null; @@ -262,18 +268,20 @@ export interface TokenMarketData { is_listed: boolean; last_price: string | null; listing: { + currency: Currency; currency_address: string | null; end_amount: string | null; end_date: number | null; is_auction: boolean; order_hash: string; - start_amount: string | null; + start_amount: string; start_date: number | null; }; owner: string; top_offer: { amount: string; currency_address: string; + currency: Currency; end_date: number; order_hash: string; start_date: number; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 99349618..3d9635ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ importers: '@ark-project/react': specifier: ^1.1.1 version: 1.1.1(typescript@5.5.4)(viem@2.17.3(typescript@5.5.4)(zod@3.23.8)) + '@avnu/avnu-sdk': + specifier: ^2.1.1 + version: 2.1.1(ethers@6.13.1)(qs@6.13.0)(starknet@6.11.0) '@hookform/error-message': specifier: ^2.0.1 version: 2.0.1(react-dom@18.3.1(react@18.3.1))(react-hook-form@7.52.1(react@18.3.1))(react@18.3.1) @@ -397,7 +400,7 @@ importers: version: 7.35.0(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-react-hooks: specifier: rc - version: 5.1.0-rc-459fd418-20241001(eslint@9.9.1(jiti@1.21.6)) + version: 5.1.0-rc-45804af1-20241021(eslint@9.9.1(jiti@1.21.6)) eslint-plugin-turbo: specifier: ^2.0.13 version: 2.1.0(eslint@9.9.1(jiti@1.21.6)) @@ -506,6 +509,14 @@ packages: '@ark-project/react@1.1.1': resolution: {integrity: sha512-YCN0+9RexyDjgidEqv6hnoVeVmKPqqYFWKEZTg19jx/t+JmvminACbUHqWWjArMjbCYbe/v/fYLA1+GsaOWpAA==} + '@avnu/avnu-sdk@2.1.1': + resolution: {integrity: sha512-y/r/pVT2pU33fGHNVE7A5UIAqQhjEXYQhUh7EodY1s5H7mhRd5U8zHOtI5z6vmpuSnUv0hSvOmmgz8HTuwZ7ew==} + engines: {node: '>=18'} + peerDependencies: + ethers: ^6.11.1 + qs: ^6.12.0 + starknet: ^6.6.0 + '@babel/code-frame@7.24.7': resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} engines: {node: '>=6.9.0'} @@ -3184,8 +3195,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 - eslint-plugin-react-hooks@5.1.0-rc-459fd418-20241001: - resolution: {integrity: sha512-vBUzji1JDwLxFAsmVtdbWQBo6LMnN7J2ZpDrGWo/ve4NnJGDeNFQOGqMJM1j0uDzLC1/sgQcFEOHofb8BsvJUQ==} + eslint-plugin-react-hooks@5.1.0-rc-45804af1-20241021: + resolution: {integrity: sha512-q8MQ4OQ0TYmKT6eS2ZObxmA+OvUrd27MN72zU5b3ib7Q+1I78C9aUzy1GelM8ttgkEkwSxE+qDBgV+K84tjt+Q==} engines: {node: '>=10'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 @@ -4590,6 +4601,10 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.13.0: + resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} + engines: {node: '>=0.6'} + query-string@7.1.3: resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==} engines: {node: '>=6'} @@ -5755,6 +5770,12 @@ snapshots: - typescript - viem + '@avnu/avnu-sdk@2.1.1(ethers@6.13.1)(qs@6.13.0)(starknet@6.11.0)': + dependencies: + ethers: 6.13.1 + qs: 6.13.0 + starknet: 6.11.0 + '@babel/code-frame@7.24.7': dependencies: '@babel/highlight': 7.24.7 @@ -8801,7 +8822,7 @@ snapshots: safe-regex-test: 1.0.3 string.prototype.includes: 2.0.0 - eslint-plugin-react-hooks@5.1.0-rc-459fd418-20241001(eslint@9.9.1(jiti@1.21.6)): + eslint-plugin-react-hooks@5.1.0-rc-45804af1-20241021(eslint@9.9.1(jiti@1.21.6)): dependencies: eslint: 9.9.1(jiti@1.21.6) @@ -10378,6 +10399,10 @@ snapshots: punycode@2.3.1: {} + qs@6.13.0: + dependencies: + side-channel: 1.0.6 + query-string@7.1.3: dependencies: decode-uri-component: 0.2.2 From 58aec36abf4028a2a7372d30ed9f1b48f6a1dadf Mon Sep 17 00:00:00 2001 From: paul launay Date: Tue, 19 Nov 2024 16:37:14 +0100 Subject: [PATCH 2/3] feat(market): display collection token currency --- .../components/collection-items-data-grid-view.tsx | 7 +++++-- apps/arkmarket/src/types/index.ts | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data-grid-view.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data-grid-view.tsx index d5865009..b56626bd 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data-grid-view.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-data-grid-view.tsx @@ -110,7 +110,9 @@ export default function CollectionItemsDataGridView({ )} > {formatUnits(token.price, 18)}{" "} - ETH + + {token.listing.currency.symbol} + ) : (
Last {viewType === "large-grid" ? "sale" : ""}{" "} - {formatUnits(token.last_price, 18)} ETH + {formatUnits(token.last_price, 18)}{" "} + {token.listing.currency.symbol} ) : null}

diff --git a/apps/arkmarket/src/types/index.ts b/apps/arkmarket/src/types/index.ts index 3f1bdf9b..123e1a64 100644 --- a/apps/arkmarket/src/types/index.ts +++ b/apps/arkmarket/src/types/index.ts @@ -111,6 +111,7 @@ export interface CollectionToken { listed_at?: number; listing: { is_auction: boolean; + currency: Currency; }; metadata?: TokenMetadata; owner: string; From ba8bc7e964dc9e08c744aa8484b2a6b491e11b56 Mon Sep 17 00:00:00 2001 From: paul launay Date: Tue, 19 Nov 2024 17:40:59 +0100 Subject: [PATCH 3/3] fix(market): fix type issues --- .../src/__tests__/buy-now-dialog.test.tsx | 102 ++++++++++-------- .../components/collection-items-buy-now.tsx | 5 +- .../token-actions-cancel-listing.tsx | 8 +- .../components/token-actions-make-bid.tsx | 10 +- .../components/tokens-actions-buy-now.tsx | 19 ++-- apps/arkmarket/src/types/index.ts | 9 +- 6 files changed, 77 insertions(+), 76 deletions(-) diff --git a/apps/arkmarket/src/__tests__/buy-now-dialog.test.tsx b/apps/arkmarket/src/__tests__/buy-now-dialog.test.tsx index cc0b77f4..5dbe314d 100644 --- a/apps/arkmarket/src/__tests__/buy-now-dialog.test.tsx +++ b/apps/arkmarket/src/__tests__/buy-now-dialog.test.tsx @@ -1,40 +1,45 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-explicit-any */ -import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; -import { vi, describe, it, expect } from "vitest"; +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; + +import type { CollectionToken, Token } from "~/types"; +import { ETH } from "~/constants/tokens"; import BuyNowDialog from "../components/buy-now-dialog"; -import type { Token, CollectionToken } from "~/types"; -vi.mock('@ark-market/ui/button', () => ({ - Button: ({ children, onClick }: any) => , +vi.mock("@ark-market/ui/button", () => ({ + Button: ({ children, onClick }: any) => ( + + ), })); -vi.mock('@ark-market/ui/dialog', () => ({ - Dialog: ({ children, open }: any) => open ?
{children}
: null, +vi.mock("@ark-market/ui/dialog", () => ({ + Dialog: ({ children, open }: any) => (open ?
{children}
: null), DialogContent: ({ children }: any) =>
{children}
, DialogTitle: ({ children }: any) =>

{children}

, })); -vi.mock('@ark-market/ui/icons', () => ({ +vi.mock("@ark-market/ui/icons", () => ({ LoaderCircle: () =>
LoaderCircle
, NoListing: () =>
NoListing
, Success: () =>
Success
, })); -vi.mock('~/app/token/[contractAddress]/[tokenId]/components/token-actions-token-overview', () => ({ - default: () =>
TokenActionsTokenOverview
, -})); - +vi.mock( + "~/app/token/[contractAddress]/[tokenId]/components/token-actions-token-overview", + () => ({ + default: () =>
TokenActionsTokenOverview
, + }), +); describe("BuyNowDialog", () => { const mockToken: Token = { collection_image: "https://example.com/image.jpg", collection_name: "Test Collection", - collection_address: '0x1234567890123456789012345678901234567890', - owner: '0x0987654321098765432109876543210987654321', - token_id: '1', - last_price: '1000000000000000000', + collection_address: "0x1234567890123456789012345678901234567890", + owner: "0x0987654321098765432109876543210987654321", + token_id: "1", + last_price: "1000000000000000000", price: "1500000000000000000", metadata: { name: "Test Token", @@ -48,18 +53,23 @@ describe("BuyNowDialog", () => { const mockCollectionToken: CollectionToken = { buy_in_progress: false, - collection_address: '0x1234567890123456789012345678901234567890', - collection_name: 'Test Collection', + collection_address: "0x1234567890123456789012345678901234567890", + collection_name: "Test Collection", floor_difference: 10, is_listed: true, listed_at: Date.now(), listing: { is_auction: false, + currency: { + contract: ETH, + decimals: 18, + symbol: "ETH", + }, }, - owner: '0x0987654321098765432109876543210987654321', - token_id: '2', - last_price: '1000000000000000000', - price: '1500000000000000000', + owner: "0x0987654321098765432109876543210987654321", + token_id: "2", + last_price: "1000000000000000000", + price: "1500000000000000000", metadata: { name: "Test Collection Token", image: "https://example.com/collection-token-image.jpg", @@ -72,7 +82,7 @@ describe("BuyNowDialog", () => { const mockSetIsOpen = vi.fn(); - it('renders the dialog for Token when open', () => { + it("renders the dialog for Token when open", () => { render( { isSuccess={false} token={mockToken} price="1500000000000000000" - /> + />, ); - expect(screen.getByText('Confirm your purchase')).toBeInTheDocument(); - expect(screen.getByText('NoListing')).toBeInTheDocument(); - expect(screen.getByText('TokenActionsTokenOverview')).toBeInTheDocument(); - expect(screen.getByText('Checking your payment')).toBeInTheDocument(); + expect(screen.getByText("Confirm your purchase")).toBeInTheDocument(); + expect(screen.getByText("NoListing")).toBeInTheDocument(); + expect(screen.getByText("TokenActionsTokenOverview")).toBeInTheDocument(); + expect(screen.getByText("Checking your payment")).toBeInTheDocument(); }); - it('renders the dialog for CollectionToken when open', () => { + it("renders the dialog for CollectionToken when open", () => { render( { isSuccess={false} token={mockCollectionToken} price="1500000000000000000" - /> + />, ); - expect(screen.getByText('Confirm your purchase')).toBeInTheDocument(); - expect(screen.getByText('NoListing')).toBeInTheDocument(); - expect(screen.getByText('TokenActionsTokenOverview')).toBeInTheDocument(); - expect(screen.getByText('Checking your payment')).toBeInTheDocument(); + expect(screen.getByText("Confirm your purchase")).toBeInTheDocument(); + expect(screen.getByText("NoListing")).toBeInTheDocument(); + expect(screen.getByText("TokenActionsTokenOverview")).toBeInTheDocument(); + expect(screen.getByText("Checking your payment")).toBeInTheDocument(); }); - it('renders success state correctly', () => { + it("renders success state correctly", () => { render( { isSuccess={true} token={mockToken} price="1500000000000000000" - /> + />, ); - expect(screen.getByText('Congratulations for your purchase')).toBeInTheDocument(); - expect(screen.getByText('Success')).toBeInTheDocument(); - expect(screen.getByText('Nice purchase, this NFT is now in your wallet ;)')).toBeInTheDocument(); - expect(screen.getByText('Continue to explore NFTs')).toBeInTheDocument(); + expect( + screen.getByText("Congratulations for your purchase"), + ).toBeInTheDocument(); + expect(screen.getByText("Success")).toBeInTheDocument(); + expect( + screen.getByText("Nice purchase, this NFT is now in your wallet ;)"), + ).toBeInTheDocument(); + expect(screen.getByText("Continue to explore NFTs")).toBeInTheDocument(); }); it('calls setIsOpen when "Continue to explore NFTs" button is clicked', () => { @@ -131,10 +145,10 @@ describe("BuyNowDialog", () => { isSuccess={true} token={mockToken} price="1500000000000000000" - /> + />, ); - fireEvent.click(screen.getByText('Continue to explore NFTs')); + fireEvent.click(screen.getByText("Continue to explore NFTs")); expect(mockSetIsOpen).toHaveBeenCalledWith(false); }); -}); \ No newline at end of file +}); diff --git a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-buy-now.tsx b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-buy-now.tsx index f7920318..3cf322aa 100644 --- a/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-buy-now.tsx +++ b/apps/arkmarket/src/app/collection/[collectionAddress]/components/collection-items-buy-now.tsx @@ -59,10 +59,7 @@ export default function CollectionItemsBuyNow({ return; } - if ( - !data || - data.value < BigInt(tokenMarketData.listing.start_amount ?? 0) - ) { + if (!data || data.value < BigInt(tokenMarketData.listing.start_amount)) { sonner.error("Insufficient balance"); return; } diff --git a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-cancel-listing.tsx b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-cancel-listing.tsx index 3a340a02..a4fb221a 100644 --- a/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-cancel-listing.tsx +++ b/apps/arkmarket/src/app/token/[contractAddress]/[tokenId]/components/token-actions-cancel-listing.tsx @@ -37,9 +37,9 @@ export default function TokenActionsCancelListing({ title: "The listing could not be canceled", additionalContent: ( { - if ( - !data || - data.value < BigInt(tokenMarketData.listing.start_amount ?? 0) - ) { + if (!data || data.value < BigInt(tokenMarketData.listing.start_amount)) { sonner.error("Insufficient balance"); return; } @@ -80,9 +77,9 @@ export default function TokenActionsBuyNow({ title: "Purchase canceled", additionalContent: ( {isSuccess ? ( ); diff --git a/apps/arkmarket/src/types/index.ts b/apps/arkmarket/src/types/index.ts index 123e1a64..2cdacb2c 100644 --- a/apps/arkmarket/src/types/index.ts +++ b/apps/arkmarket/src/types/index.ts @@ -201,18 +201,15 @@ export interface PortfolioOffers { is_listed: boolean; listing: { currency_address: string | null; + currency: Currency; end_amount: string | null; end_date: number | null; is_auction: boolean; order_hash: string; - start_amount: string | null; + start_amount: string; start_date: number | null; }; - currency?: { - contract: string; - decimals: number; - symbol: string; - } | null; + currency?: Currency | null; } export interface CollectionActivity {