diff --git a/components/common/Siderbar.js b/components/common/Siderbar.js
index 1ef78bd..810d246 100644
--- a/components/common/Siderbar.js
+++ b/components/common/Siderbar.js
@@ -10,15 +10,16 @@ export default function Sidebar(props) {
let menuItems = [
{ id: "0", label: `Basic`, link: { pathname: "/account/[account]", query: { account: account } } },
{ id: "1", label: `Key`, link: { pathname: "/account/[account]/key", query: { account: account } } },
- { id: "2", label: `Token`, link: { pathname: "/account/[account]/fungible_token", query: { account: account } } },
- { id: "3", label: `Staking`, link: { pathname: "/account/[account]/staking", query: { account: account } } },
+ { id: "2", label: `Staking`, link: { pathname: "/account/[account]/staking", query: { account: account } } },
+ { id: "3", label: `Token`, link: { pathname: "/account/[account]/fungible_token", query: { account: account } } },
{ id: "4", label: `Collection`, link: { pathname: "/account/[account]/collection", query: { account: account } } },
- { id: "5", label: `Contract`, link: { pathname: "/account/[account]/contract", query: { account: account } } },
+ { id: "5", label: `Storefront`, link: { pathname: "/account/[account]/storefront", query: { account: account } } },
+ { id: "6", label: `Contract`, link: { pathname: "/account/[account]/contract", query: { account: account } } },
{
- id: "6", label: "Storage", subItems: [
- { id: "6-0", isSubItem: true, label: "Public Items", smLabel: "Public", link: { pathname: "/account/[account]/public", query: { account: account } } },
- { id: "6-1", isSubItem: true, label: "Stored Items", smLabel: "Stored", link: { pathname: "/account/[account]/storage", query: { account: account } } },
- { id: "6-2", isSubItem: true, label: "Private Items", smLabel: "Private", link: { pathname: "/account/[account]/private", query: { account: account } } },
+ id: "7", label: "Storage", subItems: [
+ { id: "7-0", isSubItem: true, label: "Public Items", smLabel: "Public", link: { pathname: "/account/[account]/public", query: { account: account } } },
+ { id: "7-1", isSubItem: true, label: "Stored Items", smLabel: "Stored", link: { pathname: "/account/[account]/storage", query: { account: account } } },
+ { id: "7-2", isSubItem: true, label: "Private Items", smLabel: "Private", link: { pathname: "/account/[account]/private", query: { account: account } } },
]
}
// { id: "5", label: `Analyzer`, link: { pathname: "/account/[account]/analyzer", query: { account: account } } },
diff --git a/components/storefront/Listing.js b/components/storefront/Listing.js
new file mode 100644
index 0000000..6b23ec2
--- /dev/null
+++ b/components/storefront/Listing.js
@@ -0,0 +1,35 @@
+import Decimal from "decimal.js"
+import Image from "next/image"
+import { useEffect, useState } from "react"
+
+// const getImageUrl = (listing) => {
+// const url = listing?.nft?.metadata?.imageUrl ?? "/token_placeholder.png"
+// return url
+// }
+
+const getPaymentTokenSymbol = (listing) => {
+ const symbol = listing?.paymentTokenInfo?.symbol ?? "UNKN"
+ return symbol.toUpperCase()
+}
+
+export default function Listing(props) {
+ const { listing, typeId } = props
+ const symbol = getPaymentTokenSymbol(listing)
+
+ return (
+
+ {/*
+
+
*/}
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/components/storefront/ListingGroup.js b/components/storefront/ListingGroup.js
new file mode 100644
index 0000000..9b9c964
--- /dev/null
+++ b/components/storefront/ListingGroup.js
@@ -0,0 +1,35 @@
+import Listing from "./Listing"
+
+const getTypeId = (listing) => {
+ if (!listing) {
+ return "Unknown"
+ }
+
+ const rawTypeId = listing.details.nftType.typeID
+ const comps = rawTypeId.split(".")
+ return comps[2]
+}
+
+export default function ListingGroup(props) {
+ const { listings } = props
+ const firstListing = listings[0]
+ const typeId = getTypeId(firstListing)
+
+ return (
+
+
+
+ {
+ listings.map((listing, index) => {
+ return (
+
+ )
+ })
+ }
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/flow/config.js b/flow/config.js
index 24cf742..55091b7 100644
--- a/flow/config.js
+++ b/flow/config.js
@@ -17,4 +17,5 @@ config({
"0xFungibleTokenSwitchboard": publicConfig.fungibleTokenSwitchboardAddress,
"0xFlowbox": publicConfig.flowboxAddress,
"0xFlowviewAccountBookmark": publicConfig.accountBookmarkAddress,
+ "0xNFTStorefrontV2": publicConfig.nftStorefrontV2Address,
})
\ No newline at end of file
diff --git a/flow/storefront_scripts.js b/flow/storefront_scripts.js
new file mode 100644
index 0000000..b53fdad
--- /dev/null
+++ b/flow/storefront_scripts.js
@@ -0,0 +1,14 @@
+import * as fcl from "@onflow/fcl"
+
+export const getListings = async (address) => {
+ const code = await (await fetch("/scripts/storefront/get_listings.cdc")).text()
+
+ const result = await fcl.query({
+ cadence: code,
+ args: (arg, t) => [
+ arg(address, t.Address)
+ ]
+ })
+
+ return result
+}
diff --git a/flow/storefront_transactions.js b/flow/storefront_transactions.js
new file mode 100644
index 0000000..83d6435
--- /dev/null
+++ b/flow/storefront_transactions.js
@@ -0,0 +1,94 @@
+import * as fcl from "@onflow/fcl"
+import { txHandler } from "./transactions"
+
+export const cleanupGhosted = async (
+ account,
+ listingIds,
+ setTransactionInProgress,
+ setTransactionStatus
+) => {
+ const txFunc = async () => {
+ return await doCleanupGhosted(account, listingIds)
+ }
+
+ return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus)
+}
+
+const doCleanupGhosted = async (account, listingIds) => {
+ const code = await (await fetch("/transactions/storefront/cleanup_ghosted.cdc")).text()
+
+ const transactionId = fcl.mutate({
+ cadence: code,
+ args: (arg, t) => [
+ arg(account, t.Address),
+ arg(listingIds, t.Array(t.UInt64))
+ ],
+ proposer: fcl.currentUser,
+ payer: fcl.currentUser,
+ limit: 9999
+ })
+
+ return transactionId
+}
+
+export const cleanupPurchased = async (
+ account,
+ listingIds,
+ setTransactionInProgress,
+ setTransactionStatus
+) => {
+ const txFunc = async () => {
+ return await doCleanupPurchased(account, listingIds)
+ }
+
+ return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus)
+}
+
+const doCleanupPurchased = async (account, listingIds) => {
+ const code = await (await fetch("/transactions/storefront/cleanup_purchased.cdc")).text()
+
+ const transactionId = fcl.mutate({
+ cadence: code,
+ args: (arg, t) => [
+ arg(account, t.Address),
+ arg(listingIds, t.Array(t.UInt64))
+ ],
+ proposer: fcl.currentUser,
+ payer: fcl.currentUser,
+ limit: 9999
+ })
+
+ return transactionId
+}
+
+
+export const cleanupExpired = async (
+ account,
+ fromIndex, toIndex,
+ setTransactionInProgress,
+ setTransactionStatus
+) => {
+ const txFunc = async () => {
+ return await doCleanupExpired(account, fromIndex, toIndex)
+ }
+
+ return await txHandler(txFunc, setTransactionInProgress, setTransactionStatus)
+}
+
+const doCleanupExpired = async (account, fromIndex, toIndex) => {
+ const code = await (await fetch("/transactions/storefront/cleanup_expired.cdc")).text()
+
+ const transactionId = fcl.mutate({
+ cadence: code,
+ args: (arg, t) => [
+ arg(account, t.Address),
+ arg(fromIndex, t.UInt64),
+ arg(toIndex, t.UInt64)
+ ],
+ proposer: fcl.currentUser,
+ payer: fcl.currentUser,
+ limit: 9999
+ })
+
+ return transactionId
+}
\ No newline at end of file
diff --git a/pages/account/[account]/storefront/index.js b/pages/account/[account]/storefront/index.js
new file mode 100644
index 0000000..7186dda
--- /dev/null
+++ b/pages/account/[account]/storefront/index.js
@@ -0,0 +1,175 @@
+import { CodeIcon } from "@heroicons/react/outline"
+import * as fcl from "@onflow/fcl"
+import Image from "next/image"
+import { useRouter } from "next/router"
+import { useEffect, useState } from "react"
+import useSWR from "swr"
+import Layout from "../../../../components/common/Layout"
+import Spinner from "../../../../components/common/Spinner"
+import { isValidFlowAddress } from "../../../../lib/utils"
+import publicConfig from "../../../../publicConfig"
+import Custom404 from "../404"
+import { getListings } from "../../../../flow/storefront_scripts"
+import ListingGroup from "../../../../components/storefront/ListingGroup"
+import { useRecoilState } from "recoil"
+import {
+ transactionStatusState,
+ transactionInProgressState
+} from "../../../../lib/atoms"
+import { cleanupGhosted, cleanupPurchased, cleanupExpired } from "../../../../flow/storefront_transactions"
+import { useSWRConfig } from 'swr'
+
+const listingsFetcher = async (funcName, address) => {
+ const listings = await getListings(address)
+ return listings
+}
+
+const groupListings = (listings) => {
+ let grouped = listings.reduce((acc, listing) => {
+ const typeId = listing.details.nftType.typeID
+ if (!acc[typeId]) {
+ acc[typeId] = [];
+ }
+
+ acc[typeId].push(listing)
+
+ return acc
+ }, {})
+
+ for (let type in grouped) {
+ grouped[type].sort((a, b) => b.listingResourceId.localeCompare(a.listingResourceId))
+ }
+
+ let sortedGroups = Object.keys(grouped)
+ .sort()
+ .map(key => grouped[key]);
+
+ return sortedGroups
+}
+
+export default function Storefront(props) {
+ const [transactionInProgress, setTransactionInProgress] = useRecoilState(transactionInProgressState)
+ const [, setTransactionStatus] = useRecoilState(transactionStatusState)
+ const router = useRouter()
+ const { account } = router.query
+ const { mutate } = useSWRConfig()
+
+ const [listings, setListings] = useState(null)
+ const [listingGroups, setListingGroups] = useState(null)
+ const [user, setUser] = useState({ loggedIn: null })
+
+ useEffect(() => fcl.currentUser.subscribe(setUser), [])
+
+ const { data: itemsData, error: itemsError } = useSWR(
+ account && isValidFlowAddress(account) ? ["listingsFetcher", account] : null, listingsFetcher
+ )
+
+ useEffect(() => {
+ if (itemsData) {
+ setListings(itemsData)
+ const groupedListings = groupListings(itemsData.validItems)
+ setListingGroups(groupedListings)
+ }
+ }, [itemsData])
+
+ if (!account) {
+ return
+ }
+
+ if (!isValidFlowAddress(account)) {
+ return
+ }
+
+ const showItems = () => {
+ if (!listingGroups) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+ <>
+ {listingGroups.length > 0 ?
+ listingGroups.map((listings, index) => {
+ return (
+
+ )
+ }) :
+
+ Nothing found
+
+ }
+ >
+ )
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {`Listings (${listingGroups ? listingGroups.flat().length : 0})`}
+
+ {
+ listings && listings.invalidItems.length > 0 ?
+ : null
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/public/scripts/storefront/get_listings.cdc b/public/scripts/storefront/get_listings.cdc
new file mode 100644
index 0000000..4c35ce8
--- /dev/null
+++ b/public/scripts/storefront/get_listings.cdc
@@ -0,0 +1,112 @@
+import NFTStorefrontV2 from 0x4eb8a10cb9f87357
+import NonFungibleToken from 0x1d7e57aa55817448
+import HWGarageCardV2 from 0xd0bcefdf1e67ea85
+import FTRegistry from 0x097bafa4e0b48eef
+
+pub struct FTInfo {
+ pub let symbol: String
+ pub let icon: String?
+
+ init(symbol: String, icon: String?) {
+ self.symbol = symbol
+ self.icon = icon
+ }
+}
+
+pub struct Listings {
+ pub let validItems: [Item]
+ pub let invalidItems: [Item]
+
+ init(validItems: [Item], invalidItems: [Item]) {
+ self.validItems = validItems
+ self.invalidItems = invalidItems
+ }
+}
+
+pub struct Item {
+ pub let listingResourceId: UInt64
+ pub let details: NFTStorefrontV2.ListingDetails
+ pub let isGhosted: Bool
+ pub let isPurchased: Bool
+ pub let isExpired: Bool
+ pub let nft: &NonFungibleToken.NFT?
+ pub let paymentTokenInfo: FTInfo?
+
+ init(
+ listingResourceId: UInt64,
+ details: NFTStorefrontV2.ListingDetails,
+ isGhosted: Bool,
+ isPurchased: Bool,
+ isExpired: Bool,
+ nft: &NonFungibleToken.NFT?,
+ paymentTokenInfo: FTInfo?
+ ) {
+ self.listingResourceId = listingResourceId
+ self.details = details
+ self.isGhosted = isGhosted
+ self.isPurchased = isPurchased
+ self.isExpired = isExpired
+ self.nft = nft
+ self.paymentTokenInfo = paymentTokenInfo
+ }
+}
+
+pub fun main(account: Address): Listings {
+ let storefrontRef = getAccount(account)
+ .getCapability<&NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}>(
+ NFTStorefrontV2.StorefrontPublicPath
+ )
+ .borrow()
+ ?? panic("Could not borrow public storefront from address")
+
+ let validItems: [Item] = []
+ let invalidItems: [Item] = []
+ let listingIds = storefrontRef.getListingIDs()
+ for id in listingIds {
+ let listing = storefrontRef.borrowListing(listingResourceID: id)
+ ?? panic("No item with that ID")
+
+ let details = listing.getDetails()
+ // SEE: https://discord.com/channels/613813861610684416/1134479469457973318/1134714856277291088
+ let isGhosted = !listing.hasListingBecomeGhosted()
+ let isPurchased = details.purchased
+ let isExpired = details.expiry <= UInt64(getCurrentBlock().timestamp)
+ if (isPurchased || isExpired || isGhosted) {
+ let item = Item(
+ listingResourceId: id,
+ details: details,
+ isGhosted: isGhosted,
+ isPurchased: isPurchased,
+ isExpired: isExpired,
+ nft: nil,
+ paymentTokenInfo: nil
+ )
+ invalidItems.append(item)
+ continue
+ }
+
+ let nft = listing.borrowNFT()
+ if (nft == nil) {
+ continue
+ }
+
+ var ftInfo: FTInfo? = nil
+ let rawFtInfo = FTRegistry.getFTInfoByTypeIdentifier(details.salePaymentVaultType.identifier)
+ if let _rawFtInfo = rawFtInfo {
+ ftInfo = FTInfo(symbol: _rawFtInfo.alias, icon: _rawFtInfo.icon)
+ }
+
+ let item = Item(
+ listingResourceId: id,
+ details: details,
+ isGhosted: isGhosted,
+ isPurchased: isPurchased,
+ isExpired: isExpired,
+ nft: nft,
+ paymentTokenInfo: ftInfo
+ )
+ validItems.append(item)
+ }
+
+ return Listings(validItems: validItems, invalidItems: invalidItems)
+}
\ No newline at end of file
diff --git a/public/transactions/storefront/cleanup_expired.cdc b/public/transactions/storefront/cleanup_expired.cdc
new file mode 100644
index 0000000..38fecae
--- /dev/null
+++ b/public/transactions/storefront/cleanup_expired.cdc
@@ -0,0 +1,18 @@
+import NFTStorefrontV2 from 0x4eb8a10cb9f87357
+
+transaction(account: Address, from: UInt64, to: UInt64) {
+ let storefrontRef: &NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}
+
+ prepare(acct: AuthAccount) {
+ self.storefrontRef = getAccount(account)
+ .getCapability<&NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}>(
+ NFTStorefrontV2.StorefrontPublicPath
+ )
+ .borrow()
+ ?? panic("Could not borrow public storefront from address")
+ }
+
+ execute {
+ self.storefrontRef.cleanupExpiredListings(fromIndex: from, toIndex: to)
+ }
+}
\ No newline at end of file
diff --git a/public/transactions/storefront/cleanup_ghosted.cdc b/public/transactions/storefront/cleanup_ghosted.cdc
new file mode 100644
index 0000000..e7c7a0c
--- /dev/null
+++ b/public/transactions/storefront/cleanup_ghosted.cdc
@@ -0,0 +1,20 @@
+import NFTStorefrontV2 from 0x4eb8a10cb9f87357
+
+transaction(account: Address, listingResourceIds: [UInt64]) {
+ let storefrontRef: &NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}
+
+ prepare(acct: AuthAccount) {
+ self.storefrontRef = getAccount(account)
+ .getCapability<&NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}>(
+ NFTStorefrontV2.StorefrontPublicPath
+ )
+ .borrow()
+ ?? panic("Could not borrow public storefront from address")
+ }
+
+ execute {
+ for id in listingResourceIds {
+ self.storefrontRef.cleanupGhostListings(listingResourceID: id)
+ }
+ }
+}
\ No newline at end of file
diff --git a/public/transactions/storefront/cleanup_purchased.cdc b/public/transactions/storefront/cleanup_purchased.cdc
new file mode 100644
index 0000000..0aa5fdb
--- /dev/null
+++ b/public/transactions/storefront/cleanup_purchased.cdc
@@ -0,0 +1,20 @@
+import NFTStorefrontV2 from 0x4eb8a10cb9f87357
+
+transaction(account: Address, listingResourceIds: [UInt64]) {
+ let storefrontRef: &NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}
+
+ prepare(acct: AuthAccount) {
+ self.storefrontRef = getAccount(account)
+ .getCapability<&NFTStorefrontV2.Storefront{NFTStorefrontV2.StorefrontPublic}>(
+ NFTStorefrontV2.StorefrontPublicPath
+ )
+ .borrow()
+ ?? panic("Could not borrow public storefront from address")
+ }
+
+ execute {
+ for id in listingResourceIds {
+ self.storefrontRef.cleanupPurchasedListings(listingResourceID: id)
+ }
+ }
+}
\ No newline at end of file
diff --git a/publicConfig.js b/publicConfig.js
index a632631..a257cb9 100644
--- a/publicConfig.js
+++ b/publicConfig.js
@@ -55,6 +55,9 @@ if (!incrementURL) throw "Missing NEXT_PUBLIC_INCREMENT_URL"
const linkURL = process.env.NEXT_PUBLIC_LINK_URL
if (!linkURL) throw "Missing NEXT_PUBLIC_LINK_URL"
+const nftStorefrontV2Address = process.env.NEXT_PUBLIC_NFTSTOREFRONTV2_ADDRESS
+if (!nftStorefrontV2Address) throw "Missing NEXT_PUBLIC_NFTSTOREFRONTV2_ADDRESS"
+
const publicConfig = {
chainEnv,
accessNodeAPI,
@@ -69,6 +72,7 @@ const publicConfig = {
fungibleTokenSwitchboardAddress,
flowboxAddress,
accountBookmarkAddress,
+ nftStorefrontV2Address,
flownsURL,
findURL,
bayouURL,