このガイドでは、Sequenceスタックのシンプルなツールを使ってカスタムマーケットプレイスを作成する手順を説明します。

これらのツールで以下のことが可能になります:

  1. ミント:Sequence Builderからウォレットへトークンをミント
  2. ウォレット認証:Web SDKを使ったユーザー認証
  3. Marketplace SDKのセットアップ:Marketplace SDKの基本的な設定ガイド
  4. コレクションの表示:マーケットプレイスで利用可能なコレクションを取得し、表示します。
  5. 出品とオファー:ユーザーがトークンを出品したり、オファーを出せるようにします。

1. ミント

最初のステップは、Sequence Builderでコレクティブルを作成し、いくつかのトークンをミントすることです。詳しくはこちらのガイドを参照し、ミントしたtokenIdを使って今後のステップで注文のクエリや実行を行います。

2. ウォレット認証

プロジェクトには、ユーザーをウォレットで認証する仕組みが必要です。

このガイドでは、GoogleやApple認証に加え、CoinbaseやMetamaskなどユーザー自身のウォレットも利用できる、Embedded WalletWeb SDKコネクタを使用します。

パッケージのインストール

すでに優れたUIと統合されたMarketplace Boilerplateから始めることもできますし、ここではReactをゼロから使う方法も順を追って説明します。

まず、任意のフォルダ名でプロジェクトを作成します。

npm
npm create vite --template react-ts

# or

pnpm create vite --template react-ts

# or

yarn create vite --template react-ts

次に、<project_name>フォルダで必要なパッケージをインストールします。

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

# or

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

# or

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

設定ファイルを作成する

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,

  },

});

使用するすべてのchainIdを追加してください。

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>,

)

認証コンポーネント

認証を完了するには、認証用コンポーネントが必要です。

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. Marketplace SDKのセットアップ

Marketplace SDKは、マーケットプレイスをアプリケーションにシームレスに統合できる包括的なツールキットです。詳しくは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

必要なCSSインポートをメインのスタイルファイルに追加します。

index.css
@import 'tailwindcss';

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

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

vite.config.tsでTailwind CSSプラグインを設定します。

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()],

});

設定ファイルを作成する

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;

}

Marketplace SDK プロバイダーを 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. コレクションの表示

マーケットプレイスのコレクションを表示するには、src/app/componentsディレクトリ内にCollectionsというフォルダを作成します。このフォルダ内に、index.tsxCollection.tsxの2つのファイルを作成します。

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>

  );

};

最後に、App.tsxにCollectionsコンポーネントを追加します。

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. 出品とオファー

このセクションでは、マーケットプレイスの出品とオファーをUIに統合します。

型定義ファイルの作成

始める前に、src/utils内にコンポーネント用の型をまとめたファイルを用意します。これを使って関数に型を付け、コードベースの一貫性と保守性を高めます。

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;

};

コレクティブル用コンポーネントのセットアップ

アイテムページとそのコンポーネントを作成するには、src/app/componentsディレクトリ内にCollectiblesという新しいフォルダを追加します。このフォルダ内に、index.tsxCollectible.tsxの2つのファイルを作成します。

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>

  );

};

最後に、出品とオファー用のコンポーネントを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;

実行とテスト

新しいページにアイテムが正常に追加され、UI上で表示されるようになりました。動作を確認するには、pnpm devを実行してください。ランディングページから任意のコレクションを選択し、その中のNFTを表示できます。出品、購入、オファーの作成や承認など、主要なフローをテストして、すべてが期待通りに動作するか確認しましょう。