カスタムマーケットプレイスの構築
このガイドでは、Sequenceスタックを使ってカスタムマーケットプレイスを構築する手順を解説します。トークンのミント、ウォレット認証、コレクションの表示、出品・購入、オファーの作成と承認、インベントリ管理など、すべてを埋め込みウォレットとWeb SDKでシームレスに統合する方法を紹介します。
このガイドでは、Sequenceスタックのシンプルなツールを使ってカスタムマーケットプレイスを作成する手順を説明します。
これらのツールで以下のことが可能になります:
- ミント:Sequence Builderからウォレットへトークンをミント
- ウォレット認証:Web SDKを使ったユーザー認証
- Marketplace SDKのセットアップ:Marketplace SDKの基本的な設定ガイド
- コレクションの表示:マーケットプレイスで利用可能なコレクションを取得し、表示します。
- 出品とオファー:ユーザーがトークンを出品したり、オファーを出せるようにします。
1. ミント
最初のステップは、Sequence Builderでコレクティブルを作成し、いくつかのトークンをミントすることです。詳しくはこちらのガイドを参照し、ミントしたtokenId
を使って今後のステップで注文のクエリや実行を行います。
2. ウォレット認証
プロジェクトには、ユーザーをウォレットで認証する仕組みが必要です。
このガイドでは、GoogleやApple認証に加え、CoinbaseやMetamaskなどユーザー自身のウォレットも利用できる、Embedded Wallet
とWeb SDK
コネクタを使用します。
パッケージのインストール
すでに優れたUIと統合されたMarketplace Boilerplateから始めることもできますし、ここではReactをゼロから使う方法も順を追って説明します。
まず、任意のフォルダ名でプロジェクトを作成します。
npm create vite --template react-ts
# or
pnpm create vite --template react-ts
# or
yarn create vite --template react-ts
次に、<project_name>フォルダ
で必要なパッケージをインストールします。
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
設定ファイルを作成する
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でレイアウトを作成します。
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>,
)
認証コンポーネント
認証を完了するには、認証用コンポーネントが必要です。
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インポートをメインのスタイルファイルに追加します。
@import 'tailwindcss';
@import '@0xsequence/marketplace-sdk/styles/preset';
@import '@0xsequence/design-system/preset';
vite.config.tsでTailwind CSSプラグインを設定します。
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()],
});
設定ファイルを作成する
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 プロバイダーと一緒に追加
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.tsx
とCollection.tsx
の2つのファイルを作成します。
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>
);
};
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
内にコンポーネント用の型をまとめたファイルを用意します。これを使って関数に型を付け、コードベースの一貫性と保守性を高めます。
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.tsx
とCollectible.tsx
の2つのファイルを作成します。
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>
);
};
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
ファイルに追加します。
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を表示できます。出品、購入、オファーの作成や承認など、主要なフローをテストして、すべてが期待通りに動作するか確認しましょう。
このページは役に立ちましたか?