En esta guía repasaremos el proceso de crear un marketplace personalizado usando algunas herramientas simples del stack de Sequence.

Estas herramientas le permitirán realizar:

  1. Minteo: Minteo de tokens a su wallet desde el Sequence Builder
  2. Autenticación de Wallet: Uso del Web SDK para autenticar a un usuario
  3. Configuración del Marketplace SDK: Guía básica para configurar el Marketplace SDK
  4. Visualización de colecciones: Obtener y mostrar las colecciones disponibles en el marketplace.
  5. Publicaciones y Ofertas: Permita que los usuarios publiquen tokens en venta o realicen ofertas

1. Minteo

El primer paso es crear un coleccionable desde el Sequence Builder y mintear algunos tokens, lo cual puede lograrse con esta guía y usar el tokenId que minteó en los siguientes pasos para consultar y cumplir órdenes.

2. Autenticación de Wallet

Para su proyecto, necesitará una forma de autenticar a su usuario con un wallet.

Para esta guía usaremos un Embedded Wallet con conector Web SDK, que puede autenticar usuarios usando inicio de sesión con redes sociales de Google o Apple, además de wallets traídas por el usuario como Coinbase o Metamask.

Instalar Paquetes

Puede comenzar con una excelente base usando nuestro Marketplace Boilerplate, que ya incluye todo esto integrado con una gran interfaz, o aquí le mostraremos cómo usar React desde cero.

Comience creando un proyecto en una carpeta con el nombre que prefiera:

npm
npm create vite --template react-ts

# or

pnpm create vite --template react-ts

# or

yarn create vite --template react-ts

Luego, instale los paquetes requeridos en la carpeta <project_name>

pnpm
npm install @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query

# or

pnpm install @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query

# or

yarn add @0xsequence/connect @0xsequence/hooks wagmi ethers viem 0xsequence @tanstack/react-query

Cree una configuración

config.ts
import { createConfig } from "@0xsequence/connect";



const projectAccessKey = import.meta.env.VITE_PROJECT_ACCESS_KEY;

const waasConfigKey = import.meta.env.VITE_WAAS_CONFIG_KEY;

const googleClientId = import.meta.env.VITE_GOOGLE_CLIENT_ID;

const walletConnectId = import.meta.env.VITE_WALLET_CONNECT_ID;



export const config: any = createConfig("waas", {

  projectAccessKey,

  chainIds: [1, 137],

  defaultChainId: 1,

  appName: "Demo Dapp",

  waasConfigKey, // for waas

  google: {

    clientId: googleClientId,

  },

  walletConnect: {

    projectId: walletConnectId,

  },

});

Asegúrese de agregar todos los chainIds que va a utilizar

Cree un layout en el main.tsx

main.tsx
import { StrictMode } from 'react'

import { createRoot } from 'react-dom/client'

import App from './App.tsx'

import "./index.css";

import { config } from "./config";

import { SequenceConnect } from "@0xsequence/connect";



function Dapp() {

  return (

    <SequenceConnect config={config}>

      <App />

    </SequenceConnect>

  );

}



createRoot(document.getElementById('root')!).render(

  <StrictMode>

    <Dapp />

  </StrictMode>,

)

Componente de Autenticación

Para completar la autenticación, necesitará el componente de autenticación

App.tsx
import { useAccount, useDisconnect } from "wagmi";

import "./App.css";

import { useOpenConnectModal } from "@0xsequence/connect";



function App() {

  const { setOpenConnectModal } = useOpenConnectModal();

  const { isConnected, address } = useAccount();

  const { disconnect } = useDisconnect();



  function handleDisconnect() {

    disconnect();

  }



  if (isConnected && address)

    return (

      <>

        <p>User Address: {address}</p>

        <button onClick={handleDisconnect}>Disconnect</button>

      </>

    );



  return (

    <>

      <button onClick={() => setOpenConnectModal(true)}>Connect</button>

    </>

  );

}



export default App;

3. Configuración del Marketplace SDK

Marketplace SDK es un conjunto de herramientas completo que integra fácilmente nuestros Marketplaces en aplicaciones. Más información sobre Marketplace SDK

npm install @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite

# or

pnpm add @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite

# or

yarn add @0xsequence/connect @0xsequence/checkout @0xsequence/wallet-widget @0xsequence/marketplace-sdk @0xsequence/design-system @0xsequence/network @0xsequence/indexer @0xsequence/metadata wagmi ethers@^6 viem 0xsequence @tanstack/react-query @tanstack/react-query-devtools @legendapp/state framer-motion pino-pretty tailwindcss @tailwindcss/vite

Agregue las importaciones CSS requeridas a su archivo principal de estilos

index.css
@import 'tailwindcss';

@import '@0xsequence/marketplace-sdk/styles/preset';

@import '@0xsequence/design-system/preset';

Configure el plugin de Tailwind CSS en vite.config.ts

vite.config.ts
import { defineConfig } from "vite";

import react from "@vitejs/plugin-react";

import tailwindcss from "@tailwindcss/vite";



// https://vite.dev/config/

export default defineConfig({

  plugins: [react(), tailwindcss()],

});

Cree una configuración

config.ts
import type { SdkConfig } from '@0xsequence/marketplace-sdk';



const projectAccessKey = import.meta.env.VITE_PROJECT_ACCESS_KEY;

const projectId = import.meta.env.VITE_PROJECT_ID!;

const walletConnectId = import.meta.env.VITE_WALLET_CONNECT_ID;



export function getConfig() {

  const config = {

    projectId,

    projectAccessKey,

    walletConnectProjectId: walletConnectId

  } satisfies SdkConfig;



  return config;

}

Agregue los proveedores de Marketplace SDK junto con los proveedores de su Web SDK

App.tsx
import { StrictMode } from 'react'

import { createRoot } from 'react-dom/client'

import App from './App.tsx'

import "./index.css";

import { config } from "./config";

import { SequenceConnect } from "@0xsequence/connect";

import {

  MarketplaceProvider,

  ModalProvider,

} from "@0xsequence/marketplace-sdk/react";

import { getConfig } from "./config";

import { ThemeProvider, ToastProvider } from '@0xsequence/design-system';



const sdkConfig = getConfig();



function Dapp() {

  return (

    <ThemeProvider>

      <ToastProvider>

        <SequenceConnect config={config}>

          <MarketplaceProvider config={sdkConfig}>

            <App />

            <ModalProvider />

          </MarketplaceProvider>

        </SequenceConnect>

      </ToastProvider>

    </ThemeProvider>

  );

}



createRoot(document.getElementById('root')!).render(

  <StrictMode>

    <Dapp />

  </StrictMode>,

)

4. Visualización de colecciones

Para mostrar las colecciones del marketplace, necesitamos crear una carpeta llamada Collections dentro del directorio src/app/components. Dentro de esta carpeta, crearemos dos archivos: index.tsx y Collection.tsx.

Collection.tsx
import type { MarketplaceCollection } from "@0xsequence/marketplace-sdk";

import { useCollection } from "@0xsequence/marketplace-sdk/react";



export const Collection = ({

  collection,

  onSelectCollection,

}: {

  collection: MarketplaceCollection;

  onSelectCollection: (value: MarketplaceCollection) => void;

}) => {

  const { data } = useCollection({

    chainId: collection.chainId,

    collectionAddress: collection.address,

  });



  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing

  const logoURI = data?.logoURI;

  // You can add a placeholder image to improve the UX

  const image = data?.extensions.ogImage || collection.bannerUrl || logoURI;

  const description = data?.extensions.description;

  const name = data?.name;

  const symbol = data?.symbol;

  const contractType = data?.type;



  function handleOnSelectCollection() {

    onSelectCollection(collection);

  }



  return (

    <div

      onClick={handleOnSelectCollection}

      className="flex flex-col gap-2 border rounded-lg cursor-pointer w-[400px]"

    >

      <h2>

        {name} {symbol && <span>({symbol})</span>}

      </h2>

      <p>{collection.address}</p>

      <p>Chain ID: {collection.chainId}</p>

      <img

        src={image}

        alt={collection.address}

        className="w-[200px] h-[200px] object-cover object-center"

      />

      <p>Contract type: {contractType}</p>

      <p>{description}</p>

    </div>

  );

};
index.tsx
import type { MarketplaceCollection } from "@0xsequence/marketplace-sdk";

import { Collection } from "./Collection";

import { useMarketplaceConfig } from "@0xsequence/marketplace-sdk/react";



export const Collections = ({ onSelectCollection } : { onSelectCollection: (value: MarketplaceCollection) => void }) => {

	const { data } = useMarketplaceConfig();



	const collections: MarketplaceCollection[] = data?.collections || [];

  return (

    <div>

      {collections?.map((collection) => (

        <Collection key={collection.address} collection={collection} onSelectCollection={onSelectCollection}/>

      ))}

    </div>

  );

};

Para finalizar, agregaremos el componente Collections en el App.tsx

import { useAccount, useDisconnect } from "wagmi";

import "./App.css";

import { useOpenConnectModal } from "@0xsequence/connect";

import { Collections } from "./components/Collections";

import type { MarketplaceCollection } from "@0xsequence/marketplace-sdk";

import { useState } from "react";

import { Collectibles } from "./components/Collectibles";

import type { Address } from "viem";



function App() {

  const { setOpenConnectModal } = useOpenConnectModal();

  const { isConnected, address } = useAccount();

  const { disconnect } = useDisconnect();

  const [collectionSelected, setCollectionSelected] =

    useState<MarketplaceCollection | null>(null);



  function handleDisconnect() {

    disconnect();

  }

  

  function onSelectCollection(value: MarketplaceCollection) {

    setCollectionSelected(value);

  }



  function onRestartSelectedCollectionValue() {

    setCollectionSelected(null);

  }



  if (!isConnected && !address) {

    return (

      <>

        <button onClick={() => setOpenConnectModal(true)}>Connect</button>

      </>

    );

  }



  return (

    <>

      <p>User Address: {address}</p>

      <button onClick={handleDisconnect}>Disconnect</button>

      {!collectionSelected ? (

        <Collections onSelectCollection={onSelectCollection} />

      ) : (

        <>

          <button onClick={onRestartSelectedCollectionValue}>Show Collections</button>

          <div>{JSON.stringify(collectionSelected)}</div>

        </>

      )}

    </>

  );

}



export default App;

5. Listados y Ofertas

Esta sección es para integrar las publicaciones y ofertas de nuestro marketplace en la interfaz de usuario.

Cree un archivo de tipos

Antes de comenzar esto, necesitamos un archivo con algunos tipos para nuestros componentes dentro de src/utils. Usaremos esto para tipar nuestras funciones y mantener el código consistente y fácil de mantener.

types.ts
import type {

  MarketplaceKind,

  Order,

  OrderbookKind,

  Step,

} from "@0xsequence/marketplace-sdk";

import type { Address, Hash, Hex } from "viem";



type ModalCallbacks = {

  onSuccess?: ({ hash, orderId }: { hash?: Hash; orderId?: string }) => void;

  onError?: (error: Error) => void;

  onBuyAtFloorPrice?: () => void;

};



export type ShowBuyModalArgs = {

  orderId: string;

  chainId: number;

  collectionAddress: Address;

  collectibleId: string;

  marketplace: MarketplaceKind;

  customCreditCardProviderCallback?: (buyStep: Step) => void;

  skipNativeBalanceCheck?: boolean;

  nativeTokenAddress?: Address;

};



export type ShowCreateListingModalArgs = {

  collectionAddress: Hex;

  chainId: number;

  collectibleId: string;

  orderbookKind?: OrderbookKind;

  callbacks?: ModalCallbacks;

};

export type ShowMakeOfferModalArgs = {

  collectionAddress: Hex;

  chainId: number;

  collectibleId: string;

  orderbookKind?: OrderbookKind;

  callbacks?: ModalCallbacks;

};



export type ShowSellModalArgs = {

  collectionAddress: Hex;

  chainId: number;

  tokenId: string;

  order: Order;

  callbacks?: ModalCallbacks;

};

Configure los componentes de coleccionables

Para crear la página de Items y sus componentes, necesitamos agregar una nueva carpeta llamada Collectibles dentro del directorio src/app/components. Dentro de esta carpeta, crearemos dos archivos: index.tsx y Collectible.tsx.

Collectible.tsx
import type {

  ShowBuyModalArgs,

  ShowCreateListingModalArgs,

  ShowMakeOfferModalArgs,

  ShowSellModalArgs,

} from "../../utils/types";

import type {

  CollectibleOrder,

  MarketplaceKind,

  OrderbookKind,

} from "@0xsequence/marketplace-sdk";

import { useBalanceOfCollectible } from "@0xsequence/marketplace-sdk/react";

import type { Address } from "viem";



export const Collectible = ({

  collectible,

  chainId,

  collectionAddress,

  showBuyModal,

  showListModal,

  showOfferModal,

  showSellModal,

  address,

  isConnected,

  orderbookKind,

}: {

  collectible: CollectibleOrder;

  chainId: string;

  collectionAddress: Address;

  showBuyModal: (args: ShowBuyModalArgs) => void;

  showListModal: (args: ShowCreateListingModalArgs) => void;

  showOfferModal: (args: ShowMakeOfferModalArgs) => void;

  showSellModal: (args: ShowSellModalArgs) => void;

  address?: Address;

  isConnected: boolean;

  orderbookKind: OrderbookKind;

}) => {

  const { name, image, tokenId } = collectible.metadata;



  // @ts-ignore: unused variable 'isBalanceLoading'

  const { data: userBalanceResp, isLoading: isBalanceLoading } =

    useBalanceOfCollectible({

      chainId: Number(chainId),

      collectionAddress,

      collectableId: tokenId,

      userAddress: address,

      query: {

        enabled: !!isConnected && !!address,

      },

    });



  const tokenBalance = userBalanceResp?.balance;



  const onClickBuy = () =>

    showBuyModal({

      chainId: Number(chainId),

      collectionAddress,

      collectibleId: tokenId,

      orderId: collectible!.order!.orderId,

      marketplace: orderbookKind as unknown as MarketplaceKind,

    });



  const onClickList = () => {

    showListModal({

      collectionAddress,

      chainId: Number(chainId),

      collectibleId: tokenId,

      orderbookKind,

    });

  };



  const onClickOffer = () => {

    showOfferModal({

      collectionAddress,

      chainId: Number(chainId),

      collectibleId: tokenId,

      orderbookKind,

    });

  };



  const onAcceptOffer = () => {

    showSellModal({

      collectionAddress,

      chainId: Number(chainId),

      tokenId,

      order: collectible!.offer!,

    });

  };



  const hasOffer = Boolean(collectible?.offer);

  const sellDisabled = !isConnected || !hasOffer || !tokenBalance;

  const showActionButtons = address && isConnected;



  return (

    <div className="flex flex-col justify-center items-center border border-white">

      <h2>{name}</h2>

      <img

        src={image}

        alt={name}

        className="w-[200px] h-[200px] object-cover object-center"

      />

      {showActionButtons && (

        <>

          {collectible.order && <button onClick={onClickBuy}>Buy</button>}

          {tokenBalance && <button onClick={onClickList}>List</button>}

          <button onClick={onClickOffer}>Make offer</button>

          {!sellDisabled && (

            <button onClick={onAcceptOffer}>Accept Offer</button>

          )}

        </>

      )}

    </div>

  );

};
index.tsx
import { OrderbookKind, OrderSide } from "@0xsequence/marketplace-sdk";

import {

  useBuyModal,

  useCreateListingModal,

  useListCollectibles,

  useMakeOfferModal,

  useMarketplaceConfig,

  useSellModal,

} from "@0xsequence/marketplace-sdk/react";

import { Collectible } from "./Collectible";

import type { Address } from "viem";

import { useAccount } from "wagmi";



export const Collectibles = ({

  collectionId,

  chainId,

}: {

  collectionId: Address;

  chainId: number;

}) => {

  const { address, isConnected } = useAccount();

  const {

    data: collectibles,

    // @ts-ignore: unused variable 'collectiblesLoading'

    isLoading: collectiblesLoading,

    // @ts-ignore: unused variable 'fetchNextCollectibles'

    fetchNextPage: fetchNextCollectibles,

  } = useListCollectibles({

    chainId: Number(chainId),

    collectionAddress: collectionId,

    filter: {

      // # Optional filters

      includeEmpty: true,

      // searchText: text,

      // properties,

    },

    side: OrderSide.listing,

  });



  const { data } = useMarketplaceConfig();



  const onError = (error: Error) => {

    console.error(error.message);

  };



  const { show: showBuyModal } = useBuyModal({

    onSuccess(hash) {

      console.log("Buy transaction sent with hash: ", hash);

    },

    onError,

  });



  const { show: showListModal } = useCreateListingModal({ onError });

  const { show: showOfferModal } = useMakeOfferModal({

    onError,

  });

  const { show: showSellModal } = useSellModal({ onError });



  const collectiblesFlat =

    collectibles?.pages.flatMap((p) => p.collectibles) ?? [];

  const collectionData =

    data?.collections?.find(

      (collection) => collection.address === collectionId

    ) || null;

  const orderbookKind: OrderbookKind =

    (collectionData?.destinationMarketplace || "") as unknown as OrderbookKind;



  return (

    <div>

      <h1 className="text-[32px] font-semibold">Collectibles</h1>

      {collectiblesFlat?.map((collectible) => (

        <Collectible

          key={collectible.metadata.tokenId}

          collectible={collectible}

          chainId={String(chainId)}

          collectionAddress={collectionId}

          showBuyModal={showBuyModal}

          showListModal={showListModal}

          showOfferModal={showOfferModal}

          showSellModal={showSellModal}

          address={address}

          isConnected={isConnected}

          orderbookKind={orderbookKind}

        />

      ))}

    </div>

  );

};

Finalmente, agregue el componente de Publicaciones y Ofertas en nuestro archivo App.tsx

App.tsx
import { useAccount, useDisconnect } from "wagmi";

import "./App.css";

import { useOpenConnectModal } from "@0xsequence/connect";

import { Collections } from "./components/Collections";

import type { MarketplaceCollection } from "@0xsequence/marketplace-sdk";

import { useState } from "react";

import { Collectibles } from "./components/Collectibles";

import type { Address } from "viem";



function App() {

  const { setOpenConnectModal } = useOpenConnectModal();

  const { isConnected, address } = useAccount();

  const { disconnect } = useDisconnect();

  const [collectionSelected, setCollectionSelected] =

    useState<MarketplaceCollection | null>(null);



  function handleDisconnect() {

    disconnect();

  }



  function onSelectCollection(value: MarketplaceCollection) {

    setCollectionSelected(value);

  }



  function onRestartSelectedCollectionValue() {

    setCollectionSelected(null);

  }



  if (!isConnected && !address) {

    return (

      <>

        <button onClick={() => setOpenConnectModal(true)}>Connect</button>

      </>

    );

  }



  return (

    <>

      <p>User Address: {address}</p>

      <button onClick={handleDisconnect}>Disconnect</button>

      {!collectionSelected ? (

        <Collections onSelectCollection={onSelectCollection} />

      ) : (

        <>

          <button onClick={onRestartSelectedCollectionValue}>

            Show Collections

          </button>

          <Collectibles

            chainId={collectionSelected.chainId}

            collectionId={collectionSelected.address as Address}

          />

        </>

      )}

    </>

  );

}



export default App;

Ejecute y pruebe

Ha agregado exitosamente los artículos a su nueva página y ahora deberían ser visibles en la interfaz. Para verlos en acción, ejecute pnpm dev. Desde la página principal, seleccione cualquiera de sus colecciones para ver los NFTs que contiene. Asegúrese de que todo funcione correctamente probando los flujos clave como publicar, comprar, hacer ofertas y aceptar ofertas en un NFT.