@net-protocol/bazaar 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.mjs CHANGED
@@ -6,6 +6,7 @@ import { NetClient } from '@net-protocol/core';
6
6
  import { Seaport } from '@opensea/seaport-js';
7
7
  import { ethers } from 'ethers';
8
8
  import { readContract } from 'viem/actions';
9
+ import { STORAGE_CONTRACT } from '@net-protocol/storage';
9
10
 
10
11
  // src/hooks/useBazaarListings.ts
11
12
 
@@ -183,6 +184,7 @@ var DEFAULT_NFT_FEE_BPS = 500;
183
184
  var BULK_SEAPORT_ORDER_STATUS_FETCHER_ADDRESS = "0x0000009112ABCE652674b4fE3eD9C765B22d11A7";
184
185
  var ERC721_OWNER_OF_HELPER_ADDRESS = "0x00000012E3eb0700925947fAF9cd1440319b4F37";
185
186
  var ERC20_BULK_BALANCE_CHECKER_ADDRESS = "0x000000B50A9f2923F2DB931391824F6D1278f712";
187
+ var NET_SEAPORT_ZONE_ADDRESS = "0x000000007F8c58fbf215bF91Bda7421A806cf3ae";
186
188
  var NET_SEAPORT_COLLECTION_OFFER_ZONE_ADDRESS = "0x000000B799ec6D7aCC1B578f62bFc324c25DFC5A";
187
189
  var NET_SEAPORT_PRIVATE_ORDER_ZONE_ADDRESS = "0x000000bC63761cbb05305632212e2f3AE2BE7a9B";
188
190
  var BAZAAR_CHAIN_CONFIGS = {
@@ -579,8 +581,6 @@ function isErc20ListingValid(orderStatus, expirationDate, tokenAmount, sellerTok
579
581
  }
580
582
  return true;
581
583
  }
582
-
583
- // src/utils/parsing.ts
584
584
  function parseListingFromMessage(message, chainId) {
585
585
  try {
586
586
  const submission = decodeSeaportSubmission(message.data);
@@ -794,8 +794,82 @@ function sortErc20ListingsByPricePerToken(listings) {
794
794
  return 0;
795
795
  });
796
796
  }
797
-
798
- // src/client/BazaarClient.ts
797
+ var ZONE_STORED_SALE_ABI = [
798
+ { type: "uint256" },
799
+ // timestamp
800
+ { type: "uint256" },
801
+ // netTotalMessageLength
802
+ { type: "uint256" },
803
+ // netTotalMessageForAppTopicLength
804
+ {
805
+ name: "zoneParameters",
806
+ type: "tuple",
807
+ internalType: "struct ZoneParameters",
808
+ components: [
809
+ { name: "orderHash", type: "bytes32", internalType: "bytes32" },
810
+ { name: "fulfiller", type: "address", internalType: "address" },
811
+ { name: "offerer", type: "address", internalType: "address" },
812
+ {
813
+ name: "offer",
814
+ type: "tuple[]",
815
+ internalType: "struct SpentItem[]",
816
+ components: [
817
+ { name: "itemType", type: "uint8", internalType: "enum ItemType" },
818
+ { name: "token", type: "address", internalType: "address" },
819
+ { name: "identifier", type: "uint256", internalType: "uint256" },
820
+ { name: "amount", type: "uint256", internalType: "uint256" }
821
+ ]
822
+ },
823
+ {
824
+ name: "consideration",
825
+ type: "tuple[]",
826
+ internalType: "struct ReceivedItem[]",
827
+ components: [
828
+ { name: "itemType", type: "uint8", internalType: "enum ItemType" },
829
+ { name: "token", type: "address", internalType: "address" },
830
+ { name: "identifier", type: "uint256", internalType: "uint256" },
831
+ { name: "amount", type: "uint256", internalType: "uint256" },
832
+ { name: "recipient", type: "address", internalType: "address payable" }
833
+ ]
834
+ },
835
+ { name: "extraData", type: "bytes", internalType: "bytes" },
836
+ { name: "orderHashes", type: "bytes32[]", internalType: "bytes32[]" },
837
+ { name: "startTime", type: "uint256", internalType: "uint256" },
838
+ { name: "endTime", type: "uint256", internalType: "uint256" },
839
+ { name: "zoneHash", type: "bytes32", internalType: "bytes32" }
840
+ ]
841
+ }
842
+ ];
843
+ function parseSaleFromStoredData(storedData, chainId) {
844
+ try {
845
+ const cleanedData = "0x" + (storedData.startsWith("0x") ? storedData.slice(2) : storedData);
846
+ const [timestamp, , , zoneParameters] = decodeAbiParameters(
847
+ ZONE_STORED_SALE_ABI,
848
+ cleanedData
849
+ );
850
+ const offerItem = zoneParameters.offer[0];
851
+ if (!offerItem) return null;
852
+ const totalConsideration = zoneParameters.consideration.reduce(
853
+ (acc, item) => acc + item.amount,
854
+ BigInt(0)
855
+ );
856
+ return {
857
+ seller: zoneParameters.offerer,
858
+ buyer: zoneParameters.fulfiller,
859
+ tokenAddress: offerItem.token,
860
+ tokenId: offerItem.identifier.toString(),
861
+ amount: offerItem.amount,
862
+ itemType: offerItem.itemType,
863
+ priceWei: totalConsideration,
864
+ price: parseFloat(formatEther(totalConsideration)),
865
+ currency: getCurrencySymbol(chainId),
866
+ timestamp: Number(timestamp),
867
+ orderHash: zoneParameters.orderHash
868
+ };
869
+ } catch {
870
+ return null;
871
+ }
872
+ }
799
873
  var CHAIN_RPC_URLS = {
800
874
  8453: ["https://base-mainnet.public.blastapi.io", "https://mainnet.base.org"],
801
875
  84532: ["https://sepolia.base.org"],
@@ -858,7 +932,7 @@ var BazaarClient = class {
858
932
  const bazaarAddress = getBazaarAddress(this.chainId);
859
933
  const filter = {
860
934
  appAddress: bazaarAddress,
861
- topic: nftAddress.toLowerCase(),
935
+ topic: nftAddress?.toLowerCase(),
862
936
  maker
863
937
  };
864
938
  let startIndex;
@@ -931,18 +1005,57 @@ var BazaarClient = class {
931
1005
  }
932
1006
  const openListings = listings.filter((l) => l.orderStatus === 2 /* OPEN */);
933
1007
  const expiredListings = includeExpired ? listings.filter((l) => l.orderStatus === 1 /* EXPIRED */) : [];
934
- const tokenIds = openListings.map((l) => l.tokenId);
935
- const owners = await bulkFetchNftOwners(this.client, nftAddress, tokenIds);
1008
+ let validOpenListings;
936
1009
  const beforeOwnership = openListings.length;
937
- const validOpenListings = openListings.filter((listing, index) => {
938
- const owner = owners[index];
939
- return isListingValid(
940
- listing.orderStatus,
941
- listing.expirationDate,
942
- listing.maker,
943
- owner
1010
+ if (nftAddress) {
1011
+ const tokenIds = openListings.map((l) => l.tokenId);
1012
+ const owners = await bulkFetchNftOwners(this.client, nftAddress, tokenIds);
1013
+ validOpenListings = openListings.filter((listing, index) => {
1014
+ const owner = owners[index];
1015
+ return isListingValid(
1016
+ listing.orderStatus,
1017
+ listing.expirationDate,
1018
+ listing.maker,
1019
+ owner
1020
+ );
1021
+ });
1022
+ } else {
1023
+ const groups = /* @__PURE__ */ new Map();
1024
+ for (const listing of openListings) {
1025
+ const key = listing.nftAddress.toLowerCase();
1026
+ const group = groups.get(key);
1027
+ if (group) {
1028
+ group.push(listing);
1029
+ } else {
1030
+ groups.set(key, [listing]);
1031
+ }
1032
+ }
1033
+ const groupEntries = Array.from(groups.entries());
1034
+ const ownerResults = await Promise.all(
1035
+ groupEntries.map(
1036
+ ([addr, groupListings]) => bulkFetchNftOwners(
1037
+ this.client,
1038
+ addr,
1039
+ groupListings.map((l) => l.tokenId)
1040
+ )
1041
+ )
944
1042
  );
945
- });
1043
+ validOpenListings = [];
1044
+ groupEntries.forEach(([, groupListings], groupIndex) => {
1045
+ const owners = ownerResults[groupIndex];
1046
+ for (let i = 0; i < groupListings.length; i++) {
1047
+ const listing = groupListings[i];
1048
+ if (isListingValid(
1049
+ listing.orderStatus,
1050
+ listing.expirationDate,
1051
+ listing.maker,
1052
+ owners[i]
1053
+ )) {
1054
+ validOpenListings.push(listing);
1055
+ }
1056
+ }
1057
+ });
1058
+ }
946
1059
  console.log(tag, `after ownership filter: ${validOpenListings.length}/${beforeOwnership} (${beforeOwnership - validOpenListings.length} dropped)`);
947
1060
  const dedupedOpen = sortListingsByPrice(getBestListingPerToken(validOpenListings));
948
1061
  const activeTokenKeys = new Set(dedupedOpen.map((l) => `${l.nftAddress.toLowerCase()}-${l.tokenId}`));
@@ -1275,6 +1388,72 @@ var BazaarClient = class {
1275
1388
  console.log(tag, `after balance filter: ${listings.length}/${beforeBalance} (${beforeBalance - listings.length} dropped)`);
1276
1389
  return sortErc20ListingsByPricePerToken(listings);
1277
1390
  }
1391
+ /**
1392
+ * Get recent sales for a collection
1393
+ *
1394
+ * Sales are stored differently from listings: Net messages contain order hashes,
1395
+ * and the actual sale data is fetched from the bulk storage contract.
1396
+ *
1397
+ * Results are sorted by timestamp (most recent first)
1398
+ */
1399
+ async getSales(options) {
1400
+ const { nftAddress, maxMessages = 100 } = options;
1401
+ const filter = {
1402
+ appAddress: NET_SEAPORT_ZONE_ADDRESS,
1403
+ topic: nftAddress.toLowerCase()
1404
+ };
1405
+ const count = await this.netClient.getMessageCount({ filter });
1406
+ if (count === 0) {
1407
+ return [];
1408
+ }
1409
+ const startIndex = Math.max(0, count - maxMessages);
1410
+ const messages = await this.netClient.getMessages({
1411
+ filter,
1412
+ startIndex,
1413
+ endIndex: count
1414
+ });
1415
+ return this.processSalesFromMessages(messages, options);
1416
+ }
1417
+ /**
1418
+ * Process pre-fetched messages into sales.
1419
+ *
1420
+ * Each message's data field contains an order hash. The actual sale data
1421
+ * is fetched from the bulk storage contract using these order hashes.
1422
+ */
1423
+ async processSalesFromMessages(messages, options) {
1424
+ const tag = `[BazaarClient.processSales chain=${this.chainId}]`;
1425
+ if (messages.length === 0) {
1426
+ return [];
1427
+ }
1428
+ const orderHashes = messages.map((m) => m.data);
1429
+ console.log(tag, `fetching ${orderHashes.length} sale details from storage...`);
1430
+ const bulkKeys = orderHashes.map((hash) => ({
1431
+ key: hash,
1432
+ operator: NET_SEAPORT_ZONE_ADDRESS
1433
+ }));
1434
+ let storedResults;
1435
+ try {
1436
+ storedResults = await this.client.readContract({
1437
+ abi: STORAGE_CONTRACT.abi,
1438
+ address: STORAGE_CONTRACT.address,
1439
+ functionName: "bulkGet",
1440
+ args: [bulkKeys]
1441
+ });
1442
+ } catch (err) {
1443
+ console.error(tag, "bulk storage fetch failed:", err);
1444
+ return [];
1445
+ }
1446
+ const sales = [];
1447
+ for (const result of storedResults) {
1448
+ if (!result.value || result.value === "0x") continue;
1449
+ const sale = parseSaleFromStoredData(result.value, this.chainId);
1450
+ if (sale) {
1451
+ sales.push(sale);
1452
+ }
1453
+ }
1454
+ console.log(tag, `parsed ${sales.length}/${storedResults.length} sales`);
1455
+ return sales;
1456
+ }
1278
1457
  /**
1279
1458
  * Get the chain ID this client is configured for
1280
1459
  */
@@ -1421,7 +1600,7 @@ function useBazaarListings({
1421
1600
  const filter = useMemo(
1422
1601
  () => ({
1423
1602
  appAddress: bazaarAddress,
1424
- topic: nftAddress.toLowerCase(),
1603
+ topic: nftAddress?.toLowerCase(),
1425
1604
  maker
1426
1605
  }),
1427
1606
  [bazaarAddress, nftAddress, maker]
@@ -1445,7 +1624,7 @@ function useBazaarListings({
1445
1624
  endIndex,
1446
1625
  enabled: enabled && isSupported && (hasRangeOverride || totalCount > 0)
1447
1626
  });
1448
- const TAG = `[useBazaarListings chain=${chainId} nft=${nftAddress.slice(0, 10)}]`;
1627
+ const TAG = `[useBazaarListings chain=${chainId} nft=${nftAddress?.slice(0, 10) ?? "all"}]`;
1449
1628
  useEffect(() => {
1450
1629
  console.log(TAG, {
1451
1630
  enabled,
@@ -1861,7 +2040,116 @@ function useBazaarErc20Listings({
1861
2040
  refetch
1862
2041
  };
1863
2042
  }
2043
+ function useBazaarSales({
2044
+ chainId,
2045
+ nftAddress,
2046
+ maxMessages = 100,
2047
+ enabled = true
2048
+ }) {
2049
+ const wagmiClient = usePublicClient({ chainId });
2050
+ const [sales, setSales] = useState([]);
2051
+ const [isProcessing, setIsProcessing] = useState(false);
2052
+ const [processingError, setProcessingError] = useState();
2053
+ const [refetchTrigger, setRefetchTrigger] = useState(0);
2054
+ const isSupported = useMemo(
2055
+ () => isBazaarSupportedOnChain(chainId),
2056
+ [chainId]
2057
+ );
2058
+ const filter = useMemo(
2059
+ () => ({
2060
+ appAddress: NET_SEAPORT_ZONE_ADDRESS,
2061
+ topic: nftAddress.toLowerCase()
2062
+ }),
2063
+ [nftAddress]
2064
+ );
2065
+ const { count: totalCount, isLoading: isLoadingCount } = useNetMessageCount({
2066
+ chainId,
2067
+ filter,
2068
+ enabled: enabled && isSupported
2069
+ });
2070
+ const startIndex = useMemo(
2071
+ () => Math.max(0, totalCount - maxMessages),
2072
+ [totalCount, maxMessages]
2073
+ );
2074
+ const {
2075
+ messages,
2076
+ isLoading: isLoadingMessages,
2077
+ error: messagesError,
2078
+ refetch: refetchMessages
2079
+ } = useNetMessages({
2080
+ chainId,
2081
+ filter,
2082
+ startIndex,
2083
+ endIndex: totalCount,
2084
+ enabled: enabled && isSupported && totalCount > 0
2085
+ });
2086
+ const TAG = `[useBazaarSales chain=${chainId} nft=${nftAddress.slice(0, 10)}]`;
2087
+ useEffect(() => {
2088
+ console.log(TAG, {
2089
+ enabled,
2090
+ isSupported,
2091
+ totalCount,
2092
+ isLoadingCount,
2093
+ startIndex,
2094
+ endIndex: totalCount,
2095
+ messagesLength: messages?.length ?? 0,
2096
+ isLoadingMessages,
2097
+ messagesError: messagesError?.message
2098
+ });
2099
+ }, [enabled, isSupported, totalCount, isLoadingCount, startIndex, messages?.length, isLoadingMessages, messagesError]);
2100
+ useEffect(() => {
2101
+ if (!isSupported || !enabled) {
2102
+ setSales([]);
2103
+ return;
2104
+ }
2105
+ if (!messages || messages.length === 0) {
2106
+ setSales([]);
2107
+ return;
2108
+ }
2109
+ let cancelled = false;
2110
+ async function processSales() {
2111
+ setIsProcessing(true);
2112
+ setProcessingError(void 0);
2113
+ console.log(TAG, `processing ${messages.length} messages...`);
2114
+ try {
2115
+ const client = new BazaarClient({ chainId, publicClient: wagmiClient });
2116
+ const parsedSales = await client.processSalesFromMessages(
2117
+ messages,
2118
+ { nftAddress }
2119
+ );
2120
+ console.log(TAG, `processed \u2192 ${parsedSales.length} sales`);
2121
+ if (!cancelled) {
2122
+ setSales(parsedSales);
2123
+ }
2124
+ } catch (err) {
2125
+ console.error(TAG, "processing error:", err);
2126
+ if (!cancelled) {
2127
+ setProcessingError(err instanceof Error ? err : new Error(String(err)));
2128
+ setSales([]);
2129
+ }
2130
+ } finally {
2131
+ if (!cancelled) {
2132
+ setIsProcessing(false);
2133
+ }
2134
+ }
2135
+ }
2136
+ processSales();
2137
+ return () => {
2138
+ cancelled = true;
2139
+ };
2140
+ }, [chainId, nftAddress, messages, isSupported, enabled, refetchTrigger]);
2141
+ const refetch = () => {
2142
+ refetchMessages();
2143
+ setRefetchTrigger((t) => t + 1);
2144
+ };
2145
+ return {
2146
+ sales,
2147
+ isLoading: isLoadingCount || isLoadingMessages || isProcessing,
2148
+ error: messagesError || processingError,
2149
+ refetch
2150
+ };
2151
+ }
1864
2152
 
1865
- export { useBazaarCollectionOffers, useBazaarErc20Listings, useBazaarErc20Offers, useBazaarListings };
2153
+ export { useBazaarCollectionOffers, useBazaarErc20Listings, useBazaarErc20Offers, useBazaarListings, useBazaarSales };
1866
2154
  //# sourceMappingURL=react.mjs.map
1867
2155
  //# sourceMappingURL=react.mjs.map