diff --git a/examples/orderbook/create-listing-with-nextjs/package.json b/examples/orderbook/create-listing-with-nextjs/package.json index 89abe95666..e0afa729da 100644 --- a/examples/orderbook/create-listing-with-nextjs/package.json +++ b/examples/orderbook/create-listing-with-nextjs/package.json @@ -11,8 +11,8 @@ }, "dependencies": { "@biom3/react": "^0.26.1", - "@ethersproject/providers": "^5.7.2", "@imtbl/sdk": "latest", + "ethers": "^5.7.2", "next": "14.2.7", "react": "^18", "react-dom": "^18" diff --git a/examples/orderbook/create-listing-with-nextjs/src/app/create-listing-with-erc1155/page.tsx b/examples/orderbook/create-listing-with-nextjs/src/app/create-listing-with-erc1155/page.tsx index 53008d4841..dc70f9b510 100644 --- a/examples/orderbook/create-listing-with-nextjs/src/app/create-listing-with-erc1155/page.tsx +++ b/examples/orderbook/create-listing-with-nextjs/src/app/create-listing-with-erc1155/page.tsx @@ -216,7 +216,7 @@ export default function CreateERC1155ListingWithPassport() { Passport - {accountsState.length === 0 && ( + {accountsState.length === 0 ? ( + + ) : null} + {accountsState.length >= 1 ? ( + + + + ) : null} + {loading ? ( + + + + + + ) : ( + + Connected Account: + {accountsState.length >= 1 ? accountsState : "(not connected)"} + + )} + + + + + Fulfill Listing - ERC1155 Fulfillment + + {successMessage ? ( + + {successMessage} + + ) : null} + {errorMessage ? ( + + {errorMessage} + + ) : null} + + + + + NFT Contract Address + + + + Currency Type + + + + + {(listings && listings.length > 0) ? ( + + + + + SNO + Listing ID + Contract Address + Token ID + Fillable Units + Units to Fill + + + + + {listings.map((listing: any, index: number) => { + return ( + + {index + 1} + {listing.id} + {listing.sell[0].contractAddress} + {listing.sell[0].tokenId} + {listing.sell[0].amount} + + + + handleUnitsToFillChange(index, event.target.value) + } + /> + + + + + + + ); + })} + +
+
+ ) : null} + }>Return to Examples + + ); +} diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/fulfill-listing-with-erc721/page.tsx b/examples/orderbook/fulfill-listing-with-nextjs/src/app/fulfill-listing-with-erc721/page.tsx new file mode 100644 index 0000000000..a2e7582b55 --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/fulfill-listing-with-erc721/page.tsx @@ -0,0 +1,340 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { ethers } from "ethers"; +import { ProviderEvent } from "@imtbl/sdk/passport"; +import { passportInstance } from "../utils/setupPassport"; +import { orderbookSDK } from "../utils/setupOrderbook"; +import { + Box, + FormControl, + Heading, + TextInput, + Select, + Grid, + Button, + LoadingOverlay, + Link, + Table, +} from "@biom3/react"; +import NextLink from "next/link"; +import { orderbook } from "@imtbl/sdk"; +import { OrderStatusName } from "@imtbl/sdk/orderbook"; + +export default function FulfillERC721WithPassport() { + // setup the accounts state + const [accountsState, setAccountsState] = useState([]); + + // setup the loading state to enable/disable buttons when loading + const [loading, setLoadingState] = useState(false); + + // setup the loading text to display while loading + const [loadingText, setLoadingText] = useState(""); + + // fetch the Passport provider from the Passport instance + const passportProvider = passportInstance.connectEvm(); + + // create the Web3Provider using the Passport provider + const web3Provider = new ethers.providers.Web3Provider(passportProvider); + + // create the signer using the Web3Provider + const signer = web3Provider.getSigner(); + + // setup the sell item contract address state + const [sellItemContractAddress, setSellItemContractAddressState] = + useState(null); + + // setup the buy item type state + const [buyItemType, setBuyItemTypeState] = useState<"NATIVE" | "ERC20">( + "NATIVE", + ); + + // save the listings state + const [listings, setListingsState] = useState(null); + + // setup the listing creation success message state + const [successMessage, setSuccessMessageState] = useState(null); + + // setup the listing creation error message state + const [errorMessage, setErrorMessageState] = useState(null); + + const passportLogin = async () => { + if (web3Provider.provider.request) { + // disable button while loading + setLoadingState(true); + setLoadingText("Connecting to Passport"); + + // calling eth_requestAccounts triggers the Passport login flow + const accounts = await web3Provider.provider.request({ + method: "eth_requestAccounts", + }); + + // once logged in Passport is connected to the wallet and ready to transact + setAccountsState(accounts); + // reset info msg state + resetMsgState(); + // enable button when loading has finished + setLoadingState(false); + } + }; + + // listen to the ACCOUNTS_CHANGED event and update the accounts state when it changes + passportProvider.on(ProviderEvent.ACCOUNTS_CHANGED, (accounts: string[]) => { + setAccountsState(accounts); + }); + + const passportLogout = async () => { + // disable button while loading + setLoadingState(true); + setLoadingText("Logging out"); + // reset the account state + setAccountsState([]); + // reset info msg state + resetMsgState(); + // logout from passport + await passportInstance.logout(); + }; + + const resetMsgState = () => { + setSuccessMessageState(null); + setErrorMessageState(null); + }; + + // state change handlers + const handleSellItemContractAddressChange = (event: any) => { + resetMsgState(); + + const sellContractAddrsVal = + event.target.value === "" ? null : event.target.value; + setSellItemContractAddressState(sellContractAddrsVal); + }; + + const handleBuyItemTypeChange = (val: any) => { + resetMsgState(); + + setBuyItemTypeState(val); + }; + + const getListings = async ( + client: orderbook.Orderbook, + sellItemContractAddress?: string, + buyItemType?: "NATIVE" | "ERC20", + ): Promise => { + let params: orderbook.ListListingsParams = { + pageSize: 50, + sortBy: "created_at", + status: OrderStatusName.ACTIVE, + sellItemContractAddress, + buyItemType, + }; + const listings = await client.listListings(params); + return listings.result; + }; + + // memoize the listings fetch + useMemo(async () => { + const listings = await getListings( + orderbookSDK, + sellItemContractAddress, + buyItemType, + ); + const filtered = listings.filter( + (listing) => + listing.accountAddress !== accountsState[0] && + listing.sell[0].type === "ERC721", + ); + setListingsState(filtered.slice(0, 10)); + }, [accountsState, sellItemContractAddress, buyItemType]); + + const executeTrade = async (listingID: string) => { + if (accountsState.length === 0) { + setErrorMessageState("Please connect your wallet first"); + return; + } + + resetMsgState(); + setLoadingState(true); + setLoadingText("Fulfilling listing"); + + try { + await fulfillERC721Listing(listingID); + setSuccessMessageState(`Listing filled successfully`); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setErrorMessageState(message); + } + + setLoadingState(false); + }; + + // #doc fulfill-erc721-listing + // Fulfill ERC721 listing + const fulfillERC721Listing = async (listingID: string) => { + const { actions } = await orderbookSDK.fulfillOrder( + listingID, + accountsState[0], + [ + { + amount: "1000000", // Insert taker ecosystem/marketplace fee here + recipientAddress: "0x0000000000000000000000000000000000000000", // Replace address with your own marketplace address + }, + ], + ); + + for (const action of actions) { + if (action.type === orderbook.ActionType.TRANSACTION) { + const builtTx = await action.buildTransaction(); + await signer.sendTransaction(builtTx); + } + } + }; + // #enddoc fulfill-erc721-listing + + return ( + + + + Passport + + + {accountsState.length === 0 ? ( + + + + ) : null} + {accountsState.length >= 1 ? ( + + + + ) : null} + {loading ? ( + + + + + + ) : ( + + Connected Account: + {accountsState.length >= 1 ? accountsState : "(not connected)"} + + )} + + + + + Fulfill Listing - ERC721 Fulfillment + + {successMessage ? ( + + {successMessage} + + ) : null} + {errorMessage ? ( + + {errorMessage} + + ) : null} + + + + + NFT Contract Address + + + + Currency Type + + + + + {listings && listings.length > 0 ? ( + + + + + SNO + Listing ID + Contract Address + Token ID + + + + + {listings.map((listing: any, index: number) => { + return ( + + {index + 1} + {listing.id} + {listing.sell[0].contractAddress} + {listing.sell[0].tokenId} + + + + + ); + })} + +
+
+ ) : null} + }>Return to Examples +
+ ); +} diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/globals.css b/examples/orderbook/fulfill-listing-with-nextjs/src/app/globals.css new file mode 100644 index 0000000000..22fe767817 --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/globals.css @@ -0,0 +1,18 @@ +html, body { + height: 100%; +} +body { + margin: 0; +} +.flex-container { + height: 100%; + padding: 0; + margin: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.mb-1 { + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/layout.tsx b/examples/orderbook/fulfill-listing-with-nextjs/src/app/layout.tsx new file mode 100644 index 0000000000..805d7bcb7e --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/layout.tsx @@ -0,0 +1,25 @@ +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import "./globals.css"; +import AppWrapper from "./utils/wrapper"; +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "Orderbook SDK - Fulfill listing with NextJS", + description: + "Examples of how to fill a listing using the Orderbook SDK with NextJS", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/logout/page.tsx b/examples/orderbook/fulfill-listing-with-nextjs/src/app/logout/page.tsx new file mode 100644 index 0000000000..87465321cf --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/logout/page.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Box, Heading, Link } from "@biom3/react"; +import NextLink from "next/link"; + +export default function Logout() { + // render the view for after the logout is complete + return ( + + + Logged out + + }>Return to Examples + + ); +} diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/page.tsx b/examples/orderbook/fulfill-listing-with-nextjs/src/app/page.tsx new file mode 100644 index 0000000000..595f2e4af8 --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/page.tsx @@ -0,0 +1,29 @@ +"use client"; +import { Button, Heading } from "@biom3/react"; +import NextLink from "next/link"; + +export default function Home() { + return ( + <> + + Orderbook - Fulfill listing + + + + + ); +} diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/redirect/page.tsx b/examples/orderbook/fulfill-listing-with-nextjs/src/app/redirect/page.tsx new file mode 100644 index 0000000000..f4f0c0f1b4 --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/redirect/page.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { useEffect } from "react"; +import { passportInstance } from "../utils/setupPassport"; +import { Box, Heading } from "@biom3/react"; + +export default function Redirect() { + useEffect(() => { + // call the loginCallback function after the login is complete + passportInstance.loginCallback(); + }, []); + + // render the view for the login popup after the login is complete + return ( + + Logged in + + ); +} diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/setupOrderbook.ts b/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/setupOrderbook.ts new file mode 100644 index 0000000000..e14a5b88ac --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/setupOrderbook.ts @@ -0,0 +1,8 @@ +import { orderbook } from "@imtbl/sdk"; +import { Environment } from "@imtbl/sdk/config"; + +export const orderbookSDK = new orderbook.Orderbook({ + baseConfig: { + environment: Environment.SANDBOX, + }, +}); diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/setupPassport.ts b/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/setupPassport.ts new file mode 100644 index 0000000000..d6018f9ac0 --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/setupPassport.ts @@ -0,0 +1,18 @@ +import { config, passport } from "@imtbl/sdk"; + +// create the Passport instance and export it so it can be used in the examples +export const passportInstance = new passport.Passport({ + baseConfig: { + environment: config.Environment.SANDBOX, + publishableKey: process.env.NEXT_PUBLIC_PUBLISHABLE_KEY ?? "", // replace with your publishable API key from Hub + }, + clientId: process.env.NEXT_PUBLIC_CLIENT_ID ?? "", // replace with your client ID from Hub + redirectUri: "http://localhost:3000/redirect", // replace with one of your redirect URIs from Hub + logoutRedirectUri: "http://localhost:3000/logout", // replace with one of your logout URIs from Hub + audience: "platform_api", + scope: "openid offline_access email transact", + popupOverlayOptions: { + disableGenericPopupOverlay: false, // Set to true to disable the generic pop-up overlay + disableBlockedPopupOverlay: false, // Set to true to disable the blocked pop-up overlay + }, +}); diff --git a/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/wrapper.tsx b/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/wrapper.tsx new file mode 100644 index 0000000000..ae2bdafa8b --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/src/app/utils/wrapper.tsx @@ -0,0 +1,16 @@ +"use client"; +import { BiomeCombinedProviders, Stack } from "@biom3/react"; + +export default function AppWrapper({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( +
+ + {children} + +
+ ); +} diff --git a/examples/orderbook/fulfill-listing-with-nextjs/tests/base.spec.ts b/examples/orderbook/fulfill-listing-with-nextjs/tests/base.spec.ts new file mode 100644 index 0000000000..e64c9b78a9 --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/tests/base.spec.ts @@ -0,0 +1,28 @@ +import { test, expect } from "@playwright/test"; + +test.beforeEach(async ({ page }) => { + await page.goto("/"); +}); + +test.describe("home page", () => { + test("has title, heading and fulfillment links", async ({ page }) => { + await expect(page).toHaveTitle("Orderbook SDK - Fulfill listing with NextJS"); + await expect(page.getByRole("heading", { name: "Orderbook - Fulfill listing" })).toBeVisible(); + await expect(page.getByTestId("fulfill-listing-with-erc721")).toBeVisible(); + await expect(page.getByTestId("fulfill-listing-with-erc1155")).toBeVisible(); + }); +}); + +test.describe("fulfill listing with ERC721", () => { + test("loads fulfillment screen", async ({ page }) => { + await page.getByTestId("fulfill-listing-with-erc721").click(); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + }); +}); + +test.describe("fulfill listing with ERC1155", () => { + test("loads fulfillment screen", async ({ page }) => { + await page.getByTestId("fulfill-listing-with-erc1155").click(); + await expect(page.getByRole("heading", { level: 1 })).toBeVisible(); + }); +}); diff --git a/examples/orderbook/fulfill-listing-with-nextjs/tsconfig.json b/examples/orderbook/fulfill-listing-with-nextjs/tsconfig.json new file mode 100644 index 0000000000..7b28589304 --- /dev/null +++ b/examples/orderbook/fulfill-listing-with-nextjs/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/yarn.lock b/yarn.lock index 9be677cb0d..83a6299d9a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3945,7 +3945,6 @@ __metadata: resolution: "@examples/create-listing-with-nextjs@workspace:examples/orderbook/create-listing-with-nextjs" dependencies: "@biom3/react": ^0.26.1 - "@ethersproject/providers": ^5.7.2 "@imtbl/sdk": latest "@playwright/test": ^1.45.3 "@types/node": ^20 @@ -3953,6 +3952,27 @@ __metadata: "@types/react-dom": ^18 eslint: ^8 eslint-config-next: 14.2.7 + ethers: ^5.7.2 + next: 14.2.7 + react: ^18 + react-dom: ^18 + typescript: ^5 + languageName: unknown + linkType: soft + +"@examples/fulfill-listing-with-nextjs@workspace:examples/orderbook/fulfill-listing-with-nextjs": + version: 0.0.0-use.local + resolution: "@examples/fulfill-listing-with-nextjs@workspace:examples/orderbook/fulfill-listing-with-nextjs" + dependencies: + "@biom3/react": ^0.26.1 + "@imtbl/sdk": latest + "@playwright/test": ^1.45.3 + "@types/node": ^20 + "@types/react": ^18 + "@types/react-dom": ^18 + eslint: ^8 + eslint-config-next: 14.2.7 + ethers: ^5.7.2 next: 14.2.7 react: ^18 react-dom: ^18