diff --git a/apps/appkit-minter/src/core/components/layout/app-router.tsx b/apps/appkit-minter/src/core/components/layout/app-router.tsx index 81be506c6..e50f8840d 100644 --- a/apps/appkit-minter/src/core/components/layout/app-router.tsx +++ b/apps/appkit-minter/src/core/components/layout/app-router.tsx @@ -11,7 +11,15 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { useWatchBalance, useWatchTransactions, useWatchJettons } from '@ton/appkit-react'; import { toast } from 'sonner'; -import { MinterPage, StakingPage, SwapPage, OnrampPage, SignMessagePage } from '@/pages'; +import { + MinterPage, + StakingPage, + SwapPage, + OnrampPage, + SignMessagePage, + NftPurchasePage, + NftPurchaseCollectionPage, +} from '@/pages'; export const AppRouter: React.FC = () => { // Enable global real-time balance updates @@ -54,6 +62,8 @@ export const AppRouter: React.FC = () => { } /> } /> } /> + } /> + } /> } /> diff --git a/apps/appkit-minter/src/core/components/layout/app-sidebar.tsx b/apps/appkit-minter/src/core/components/layout/app-sidebar.tsx index fe459cc0f..87eb21f93 100644 --- a/apps/appkit-minter/src/core/components/layout/app-sidebar.tsx +++ b/apps/appkit-minter/src/core/components/layout/app-sidebar.tsx @@ -7,7 +7,7 @@ */ import type React from 'react'; -import { Coins, ArrowLeftRight, Sparkles, BookOpen, Github, PenLine, CreditCard } from 'lucide-react'; +import { Coins, ArrowLeftRight, Sparkles, BookOpen, Github, PenLine, CreditCard, ShoppingBag } from 'lucide-react'; import { Link, NavLink } from 'react-router-dom'; import { AppLogo } from '../app-logo'; @@ -32,6 +32,7 @@ const NAV_LINKS: readonly { to: string; label: string; icon: React.ComponentType { to: '/swap', label: 'Swap', icon: ArrowLeftRight }, { to: '/staking', label: 'Staking', icon: Coins }, { to: '/onramp', label: 'Buy', icon: CreditCard }, + { to: '/buy-nft', label: 'Buy NFT', icon: ShoppingBag }, { to: '/sign', label: 'Sign Message', icon: PenLine }, ]; diff --git a/apps/appkit-minter/src/core/configs/env.ts b/apps/appkit-minter/src/core/configs/env.ts index 8b5feeadb..07e4b8ca8 100644 --- a/apps/appkit-minter/src/core/configs/env.ts +++ b/apps/appkit-minter/src/core/configs/env.ts @@ -10,3 +10,4 @@ export const ENV_TON_API_KEY_MAINNET = import.meta.env.VITE_TON_API_KEY ?? '25a9b2326a34b39a5fa4b264fb78fb4709e1bd576fc5e6b176639f5b71e94b0d'; export const ENV_TON_API_KEY_TESTNET = import.meta.env.VITE_TON_API_TESTNET_KEY ?? 'd852b54d062f631565761042cccea87fa6337c41eb19b075e6c7fb88898a3992'; +export const ENV_GETGEMS_API_KEY: string = import.meta.env.VITE_GETGEMS_API_KEY ?? ''; diff --git a/apps/appkit-minter/src/features/nft_purchase/api/getgems-client.ts b/apps/appkit-minter/src/features/nft_purchase/api/getgems-client.ts new file mode 100644 index 000000000..5ecbeb7d0 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/api/getgems-client.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { + GetGemsBuyResponse, + GetGemsCollection, + GetGemsEnvelope, + GetGemsNftFull, + GetGemsNftsOnSaleResponse, +} from './types'; + +import { ENV_GETGEMS_API_KEY } from '@/core/configs/env'; + +// The real GetGems API (https://api.getgems.io/public-api) does not send +// CORS headers for browser origins, so we route requests through the Vite +// dev-server proxy configured in vite.config.ts. +const BASE_URL = '/getgems-api'; + +async function request(path: string, init?: RequestInit): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + ...init, + headers: { + Accept: 'application/json', + Authorization: ENV_GETGEMS_API_KEY, + ...(init?.body ? { 'Content-Type': 'application/json' } : {}), + ...init?.headers, + }, + }); + + if (!res.ok) { + const text = await res.text().catch(() => ''); + throw new Error(`GetGems ${res.status} ${res.statusText}: ${text || path}`); + } + + const body = (await res.json()) as GetGemsEnvelope | T; + + if (typeof body === 'object' && body !== null && 'success' in body) { + const envelope = body as GetGemsEnvelope; + if (!envelope.success) { + throw new Error(`GetGems request failed: ${path}`); + } + return envelope.response; + } + + return body as T; +} + +export function fetchCollection(collectionAddress: string): Promise { + return request(`/v1/collection/${encodeURIComponent(collectionAddress)}`); +} + +export function fetchNftsOnSale(collectionAddress: string, limit = 30): Promise { + return request( + `/v1/nfts/on-sale/${encodeURIComponent(collectionAddress)}?limit=${limit}`, + ); +} + +export function fetchNft(nftAddress: string): Promise { + return request(`/v1/nft/${encodeURIComponent(nftAddress)}`); +} + +export function buildBuyTransaction(nftAddress: string, version: string): Promise { + return request(`/v1/nfts/buy-fix-price/${encodeURIComponent(nftAddress)}`, { + method: 'POST', + body: JSON.stringify({ version }), + }); +} diff --git a/apps/appkit-minter/src/features/nft_purchase/api/types.ts b/apps/appkit-minter/src/features/nft_purchase/api/types.ts new file mode 100644 index 000000000..452737f49 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/api/types.ts @@ -0,0 +1,72 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export interface GetGemsEnvelope { + success: boolean; + response: T; +} + +export interface GetGemsFixPriceSale { + type?: string; + fullPrice: string; + marketplaceFee?: string; + currency: string; + version: string; + contractAddress?: string; +} + +export type GetGemsSale = GetGemsFixPriceSale | { type?: string; [key: string]: unknown }; + +export interface GetGemsNftOnSale { + address: string; + name?: string | null; + image?: string | null; + sale?: GetGemsSale | null; +} + +export interface GetGemsNftsOnSaleResponse { + items: GetGemsNftOnSale[]; + cursor?: string | null; +} + +export interface GetGemsNftFull { + address: string; + name?: string | null; + description?: string | null; + image?: string | null; + ownerAddress?: string | null; + sale?: GetGemsSale | null; +} + +export interface GetGemsCollection { + address: string; + name?: string | null; + description?: string | null; + image?: string | null; + ownerAddress?: string | null; +} + +export interface GetGemsBuyMessage { + to: string; + amount: string; + payload?: string | null; + stateInit?: string | null; +} + +export interface GetGemsBuyResponse { + uuid: string; + from?: string | null; + timeout: string; + list: GetGemsBuyMessage[]; +} + +export function isFixPriceSale(sale: GetGemsSale | null | undefined): sale is GetGemsFixPriceSale { + if (!sale) return false; + const candidate = sale as GetGemsFixPriceSale; + return typeof candidate.version === 'string' && typeof candidate.fullPrice === 'string'; +} diff --git a/apps/appkit-minter/src/features/nft_purchase/components/collection-card.tsx b/apps/appkit-minter/src/features/nft_purchase/components/collection-card.tsx new file mode 100644 index 000000000..b6e414686 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/components/collection-card.tsx @@ -0,0 +1,48 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; +import { Link } from 'react-router-dom'; +import { ChevronRight, ImageIcon } from 'lucide-react'; + +import { useCollection } from '../hooks/use-collections'; + +interface CollectionCardProps { + address: string; +} + +export const CollectionCard: FC = ({ address }) => { + const { data, isLoading, isError } = useCollection(address); + + const name = data?.name ?? (isLoading ? 'Loading…' : 'Unknown collection'); + const description = data?.description; + const image = data?.image; + + return ( + +
+ {image ? ( + {name} + ) : isLoading ? ( +
+ ) : ( + + )} +
+
+

{name}

+ {description && !isError &&

{description}

} + {isError &&

Failed to load collection info

} +
+ + + ); +}; diff --git a/apps/appkit-minter/src/features/nft_purchase/components/collections-list.tsx b/apps/appkit-minter/src/features/nft_purchase/components/collections-list.tsx new file mode 100644 index 000000000..8acbb02c3 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/components/collections-list.tsx @@ -0,0 +1,28 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { useFeaturedCollectionAddresses } from '../hooks/use-collections'; +import { CollectionCard } from './collection-card'; + +import { Card } from '@/core/components'; + +export const CollectionsList: FC = () => { + const addresses = useFeaturedCollectionAddresses(); + + return ( + +
+ {addresses.map((address) => ( + + ))} +
+
+ ); +}; diff --git a/apps/appkit-minter/src/features/nft_purchase/components/nft-card.tsx b/apps/appkit-minter/src/features/nft_purchase/components/nft-card.tsx new file mode 100644 index 000000000..744558793 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/components/nft-card.tsx @@ -0,0 +1,211 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useState } from 'react'; +import type { FC } from 'react'; +import { Button, useSelectedWallet } from '@ton/appkit-react'; +import type { TransactionRequest, TransactionRequestMessage } from '@ton/appkit'; +import { getJettonWalletAddress, Network } from '@ton/appkit'; +import { beginCell, toNano } from '@ton/core'; +import { Image as ImageIcon, ShoppingCart } from 'lucide-react'; +import { toast } from 'sonner'; + +import { buildBuyTransaction, fetchNft } from '../api/getgems-client'; +import { isFixPriceSale } from '../api/types'; +import type { GetGemsNftFull, GetGemsNftOnSale } from '../api/types'; +import { formatAmount, formatPrice, getCurrencyDecimals, safeBigInt } from '../lib/currency'; +import { buildJettonTransferBody, getJettonMaster } from '../lib/jetton'; +import { PurchaseModal } from './purchase-modal'; +import type { PurchaseDetails } from './purchase-modal'; + +import { appKit } from '@/core/configs/app-kit'; + +interface NftCardProps { + nft: GetGemsNftOnSale; +} + +const TON_DECIMALS = 9; + +/** Gas attached to the jetton_transfer (matches what GetGems' own UI sends). */ +const JETTON_BUY_GAS = toNano('0.5'); +/** TON forwarded with jetton_notify to the sale contract. */ +const JETTON_FORWARD_TON = toNano('0.35'); + +async function buildJettonBuyTransaction(args: { + nft: GetGemsNftFull; + userAddress: string; + currency: string; + priceNano: bigint; + saleContract: string; +}): Promise { + const master = getJettonMaster(args.currency); + if (!master) { + throw new Error(`Currency ${args.currency} is not supported yet`); + } + + const userJettonWallet = await getJettonWalletAddress(appKit, { + jettonAddress: master, + ownerAddress: args.userAddress, + }); + + const commentCell = beginCell().storeUint(0, 32).storeStringTail('Bought on getgems.io').endCell(); + + const payload = buildJettonTransferBody({ + queryId: BigInt(Date.now()), + jettonAmount: args.priceNano, + destination: args.saleContract, + responseDestination: args.userAddress, + forwardTonAmount: JETTON_FORWARD_TON, + forwardPayload: commentCell, + }); + + console.log('[nft_purchase] USDT buy tx', { + userJettonWallet, + saleContract: args.saleContract, + priceNano: args.priceNano.toString(), + buyGas: JETTON_BUY_GAS.toString(), + forwardTon: JETTON_FORWARD_TON.toString(), + }); + + const messages: TransactionRequestMessage[] = [ + { + address: userJettonWallet, + amount: JETTON_BUY_GAS.toString(), + payload, + }, + ]; + + return { + validUntil: Math.floor(Date.now() / 1000) + 300, + messages, + }; +} + +export const NftCard: FC = ({ nft }) => { + const [wallet] = useSelectedWallet(); + const [isLoadingBuy, setIsLoadingBuy] = useState(false); + const [details, setDetails] = useState(null); + + const sale = isFixPriceSale(nft.sale) ? nft.sale : null; + const priceLabel = sale ? `${formatPrice(sale.fullPrice, sale.currency)} ${sale.currency}` : null; + + const isMainnet = wallet?.getNetwork().chainId === Network.mainnet().chainId; + + const handleBuyClick = async () => { + if (isLoadingBuy || !wallet) return; + setIsLoadingBuy(true); + try { + const fresh = await fetchNft(nft.address); + if (!isFixPriceSale(fresh.sale)) { + toast.error('This NFT is not on sale anymore'); + return; + } + + const currency = fresh.sale.currency; + const priceDecimals = getCurrencyDecimals(currency); + const priceRaw = safeBigInt(fresh.sale.fullPrice); + + let tx: TransactionRequest; + let networkFeeRaw: bigint; + + if (currency === 'TON') { + const buy = await buildBuyTransaction(nft.address, fresh.sale.version); + const messages: TransactionRequestMessage[] = buy.list.map((item) => { + const message: TransactionRequestMessage = { address: item.to, amount: item.amount }; + if (item.payload) message.payload = item.payload; + if (item.stateInit) message.stateInit = item.stateInit; + return message; + }); + tx = { + validUntil: Math.floor(new Date(buy.timeout).getTime() / 1000), + messages, + }; + networkFeeRaw = buy.list.reduce((acc, item) => acc + safeBigInt(item.amount), 0n); + } else { + const saleContract = fresh.sale.contractAddress; + if (!saleContract) { + throw new Error('Sale contract address is missing'); + } + tx = await buildJettonBuyTransaction({ + nft: fresh, + userAddress: wallet.getAddress(), + currency, + priceNano: priceRaw, + saleContract, + }); + networkFeeRaw = JETTON_BUY_GAS; + } + + setDetails({ + nftName: fresh.name ?? nft.name ?? 'Untitled', + nftImage: fresh.image ?? nft.image, + priceAmount: formatAmount(priceRaw, priceDecimals), + priceCurrency: currency, + networkFeeTon: formatAmount(networkFeeRaw, TON_DECIMALS), + tx, + }); + } catch (error) { + toast.error(error instanceof Error ? error.message : 'Failed to prepare purchase'); + } finally { + setIsLoadingBuy(false); + } + }; + + return ( + <> +
+
+ {nft.image ? ( + {nft.name + ) : ( + + )} +
+
+

{nft.name ?? 'Untitled'}

+

{priceLabel ?? 'Not for sale'}

+ {sale && ( +
+ {!wallet ? ( + + ) : !isMainnet ? ( + + ) : ( + + )} +
+ )} +
+
+ + {details && ( + { + if (!open) setDetails(null); + }} + details={details} + onPurchased={() => setDetails(null)} + /> + )} + + ); +}; diff --git a/apps/appkit-minter/src/features/nft_purchase/components/nfts-list.tsx b/apps/appkit-minter/src/features/nft_purchase/components/nfts-list.tsx new file mode 100644 index 000000000..59d261f3d --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/components/nfts-list.tsx @@ -0,0 +1,74 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useMemo } from 'react'; +import type { FC } from 'react'; +import { AlertCircle, Image as ImageIcon } from 'lucide-react'; +import { Button } from '@ton/appkit-react'; + +import { useNftsOnSale } from '../hooks/use-nfts-on-sale'; +import { NftCard } from './nft-card'; +import { isFixPriceSale } from '../api/types'; + +import { Card } from '@/core/components'; + +interface NftsListProps { + collectionAddress: string; +} + +export const NftsList: FC = ({ collectionAddress }) => { + const { data, isLoading, isError, refetch } = useNftsOnSale(collectionAddress); + + const nfts = useMemo(() => (data?.items ?? []).filter((nft) => isFixPriceSale(nft.sale)), [data?.items]); + + if (isError) { + return ( + +
+ +

Failed to load NFTs

+ +
+
+ ); + } + + if (isLoading) { + return ( + +
+
+ Loading NFTs... +
+
+ ); + } + + if (nfts.length === 0) { + return ( + +
+ +

No NFTs currently on sale

+
+
+ ); + } + + return ( + +
+ {nfts.map((nft) => ( + + ))} +
+
+ ); +}; diff --git a/apps/appkit-minter/src/features/nft_purchase/components/purchase-modal.tsx b/apps/appkit-minter/src/features/nft_purchase/components/purchase-modal.tsx new file mode 100644 index 000000000..7b7c1c9c5 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/components/purchase-modal.tsx @@ -0,0 +1,118 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useState } from 'react'; +import type { FC } from 'react'; +import { AlertCircle, Image as ImageIcon, ShoppingCart } from 'lucide-react'; +import { Button, Modal, Send } from '@ton/appkit-react'; +import type { TransactionRequest } from '@ton/appkit'; +import { getErrorMessage } from '@ton/appkit'; +import { toast } from 'sonner'; + +export interface PurchaseDetails { + nftName: string; + nftImage?: string | null; + priceAmount: string; + priceCurrency: string; + networkFeeTon: string; + tx: TransactionRequest; +} + +interface PurchaseModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + details: PurchaseDetails; + onPurchased: () => void; +} + +export const PurchaseModal: FC = ({ open, onOpenChange, details, onPurchased }) => { + const [confirmed, setConfirmed] = useState(false); + + return ( + +
+
+
+ {details.nftImage ? ( + {details.nftName} + ) : ( + + )} +
+

{details.nftName}

+
+ +
+
+
+

NFT price

+

Includes service fee and royalties

+
+

+ {details.priceAmount} {details.priceCurrency} +

+
+
+
+

Network fee

+

Unused part will be refunded to your wallet

+
+

{details.networkFeeTon} TON

+
+
+ +
+
+ +
+

Warning

+

+ You are buying an NFT without a verification mark. It may be a counterfeit item. + Double-check the NFT before purchase. +

+
+
+ +
+ + { + toast.success('Purchase sent'); + onPurchased(); + onOpenChange(false); + }} + onError={(error: Error) => toast.error(getErrorMessage(error))} + disabled={!confirmed} + > + {({ isLoading, onSubmit, disabled }) => ( + + )} + +
+
+ ); +}; diff --git a/apps/appkit-minter/src/features/nft_purchase/hooks/use-collections.ts b/apps/appkit-minter/src/features/nft_purchase/hooks/use-collections.ts new file mode 100644 index 000000000..631acf644 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/hooks/use-collections.ts @@ -0,0 +1,26 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryResult } from '@tanstack/react-query'; + +import { fetchCollection } from '../api/getgems-client'; +import type { GetGemsCollection } from '../api/types'; +import { FEATURED_COLLECTION_ADDRESSES } from '../lib/featured-collections'; + +export function useFeaturedCollectionAddresses(): readonly string[] { + return FEATURED_COLLECTION_ADDRESSES; +} + +export function useCollection(address: string): UseQueryResult { + return useQuery({ + queryKey: ['getgems', 'collection', address], + queryFn: () => fetchCollection(address), + staleTime: 5 * 60_000, + }); +} diff --git a/apps/appkit-minter/src/features/nft_purchase/hooks/use-nfts-on-sale.ts b/apps/appkit-minter/src/features/nft_purchase/hooks/use-nfts-on-sale.ts new file mode 100644 index 000000000..891d64165 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/hooks/use-nfts-on-sale.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import { useQuery } from '@tanstack/react-query'; +import type { UseQueryResult } from '@tanstack/react-query'; + +import { fetchNftsOnSale } from '../api/getgems-client'; +import type { GetGemsNftsOnSaleResponse } from '../api/types'; + +export function useNftsOnSale(collectionAddress: string): UseQueryResult { + return useQuery({ + queryKey: ['getgems', 'nfts-on-sale', collectionAddress], + queryFn: () => fetchNftsOnSale(collectionAddress), + staleTime: 30_000, + }); +} diff --git a/apps/appkit-minter/src/features/nft_purchase/index.ts b/apps/appkit-minter/src/features/nft_purchase/index.ts new file mode 100644 index 000000000..2973e2802 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/index.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export { CollectionsList } from './components/collections-list'; +export { NftsList } from './components/nfts-list'; diff --git a/apps/appkit-minter/src/features/nft_purchase/lib/currency.ts b/apps/appkit-minter/src/features/nft_purchase/lib/currency.ts new file mode 100644 index 000000000..c1e21ce3c --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/lib/currency.ts @@ -0,0 +1,41 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +const CURRENCY_DECIMALS: Record = { + TON: 9, + USDT: 6, + NOT: 9, + DOGS: 9, + HMSTR: 9, +}; + +export function getCurrencyDecimals(currency: string): number { + return CURRENCY_DECIMALS[currency.toUpperCase()] ?? 9; +} + +export function safeBigInt(value: string): bigint { + try { + return BigInt(value); + } catch { + return 0n; + } +} + +export function formatAmount(raw: bigint, decimals: number, maxFraction = 4): string { + if (decimals <= 0) return raw.toString(); + const base = 10n ** BigInt(decimals); + const whole = raw / base; + const frac = raw % base; + const fracStr = frac.toString().padStart(decimals, '0').slice(0, Math.max(0, maxFraction)).replace(/0+$/, ''); + return fracStr ? `${whole}.${fracStr}` : `${whole}`; +} + +export function formatPrice(rawAmount: string, currency: string): string { + const decimals = getCurrencyDecimals(currency); + return formatAmount(safeBigInt(rawAmount), decimals); +} diff --git a/apps/appkit-minter/src/features/nft_purchase/lib/featured-collections.ts b/apps/appkit-minter/src/features/nft_purchase/lib/featured-collections.ts new file mode 100644 index 000000000..79bd8b6f5 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/lib/featured-collections.ts @@ -0,0 +1,9 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +export const FEATURED_COLLECTION_ADDRESSES: readonly string[] = ['EQC3dNlesgVD8YbAazcauIrXBPfiVhMMr5YYk2in0Mtsz0Bz']; diff --git a/apps/appkit-minter/src/features/nft_purchase/lib/jetton.ts b/apps/appkit-minter/src/features/nft_purchase/lib/jetton.ts new file mode 100644 index 000000000..b938c9646 --- /dev/null +++ b/apps/appkit-minter/src/features/nft_purchase/lib/jetton.ts @@ -0,0 +1,61 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { Cell } from '@ton/core'; +import { Address, beginCell } from '@ton/core'; + +export const JETTON_MASTERS: Record = { + USDT: 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs', +}; + +export function getJettonMaster(currency: string): string | undefined { + return JETTON_MASTERS[currency.toUpperCase()]; +} + +interface JettonTransferParams { + queryId: bigint; + jettonAmount: bigint; + destination: string; + responseDestination: string; + forwardTonAmount: bigint; + forwardPayload?: Cell | null; +} + +/** + * Build the body of a TEP-74 jetton_transfer internal message. + * Returns a Base64-encoded BoC ready to place in a TransactionRequestMessage.payload. + * + * For GetGems FixPriceSale USDT buys the sale contract recognizes the purchase + * from a jetton_notify with an empty forward_payload, so callers should pass + * forwardPayload=null. + */ +export function buildJettonTransferBody({ + queryId, + jettonAmount, + destination, + responseDestination, + forwardTonAmount, + forwardPayload, +}: JettonTransferParams): string { + const builder = beginCell() + .storeUint(0x0f8a7ea5, 32) + .storeUint(queryId, 64) + .storeCoins(jettonAmount) + .storeAddress(Address.parse(destination)) + .storeAddress(Address.parse(responseDestination)) + .storeBit(false) + .storeCoins(forwardTonAmount); + + if (forwardPayload) { + builder.storeBit(true).storeRef(forwardPayload); + } else { + builder.storeBit(false); + } + + return builder.endCell().toBoc().toString('base64'); +} diff --git a/apps/appkit-minter/src/pages/index.ts b/apps/appkit-minter/src/pages/index.ts index 5b512d331..335754178 100644 --- a/apps/appkit-minter/src/pages/index.ts +++ b/apps/appkit-minter/src/pages/index.ts @@ -11,3 +11,5 @@ export { SwapPage } from './swap-page'; export { StakingPage } from './staking-page'; export { OnrampPage } from './onramp-page'; export { SignMessagePage } from './sign-message-page'; +export { NftPurchasePage } from './nft-purchase-page'; +export { NftPurchaseCollectionPage } from './nft-purchase-collection-page'; diff --git a/apps/appkit-minter/src/pages/nft-purchase-collection-page.tsx b/apps/appkit-minter/src/pages/nft-purchase-collection-page.tsx new file mode 100644 index 000000000..8f817d44d --- /dev/null +++ b/apps/appkit-minter/src/pages/nft-purchase-collection-page.tsx @@ -0,0 +1,38 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; +import { Link, useParams, Navigate } from 'react-router-dom'; +import { ChevronLeft } from 'lucide-react'; + +import { NftsList } from '@/features/nft_purchase'; +import { Layout } from '@/core/components'; + +export const NftPurchaseCollectionPage: FC = () => { + const { collectionAddress } = useParams<{ collectionAddress: string }>(); + + if (!collectionAddress) { + return ; + } + + return ( + +
+ + + Back to collections + + + +
+
+ ); +}; diff --git a/apps/appkit-minter/src/pages/nft-purchase-page.tsx b/apps/appkit-minter/src/pages/nft-purchase-page.tsx new file mode 100644 index 000000000..6a5e58312 --- /dev/null +++ b/apps/appkit-minter/src/pages/nft-purchase-page.tsx @@ -0,0 +1,20 @@ +/** + * Copyright (c) TonTech. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import type { FC } from 'react'; + +import { CollectionsList } from '@/features/nft_purchase'; +import { Layout } from '@/core/components'; + +export const NftPurchasePage: FC = () => { + return ( + + + + ); +}; diff --git a/apps/appkit-minter/vite.config.ts b/apps/appkit-minter/vite.config.ts index 8541ac850..330576b1b 100644 --- a/apps/appkit-minter/vite.config.ts +++ b/apps/appkit-minter/vite.config.ts @@ -19,6 +19,13 @@ export default defineConfig({ server: { port: 5174, allowedHosts: ['localhost', '127.0.0.1', 'local.dev'], + proxy: { + '/getgems-api': { + target: 'https://api.getgems.io', + changeOrigin: true, + rewrite: (p) => p.replace(/^\/getgems-api/, '/public-api'), + }, + }, }, resolve: { alias: {