@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/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { NetClient } from '@net-protocol/core';
3
3
  import { Seaport } from '@opensea/seaport-js';
4
4
  import { ethers } from 'ethers';
5
5
  import { readContract } from 'viem/actions';
6
+ import { STORAGE_CONTRACT } from '@net-protocol/storage';
6
7
 
7
8
  // src/client/BazaarClient.ts
8
9
 
@@ -789,8 +790,6 @@ function isErc20ListingValid(orderStatus, expirationDate, tokenAmount, sellerTok
789
790
  }
790
791
  return true;
791
792
  }
792
-
793
- // src/utils/parsing.ts
794
793
  function parseListingFromMessage(message, chainId) {
795
794
  try {
796
795
  const submission = decodeSeaportSubmission(message.data);
@@ -1012,8 +1011,85 @@ function sortErc20ListingsByPricePerToken(listings) {
1012
1011
  return 0;
1013
1012
  });
1014
1013
  }
1015
-
1016
- // src/client/BazaarClient.ts
1014
+ var ZONE_STORED_SALE_ABI = [
1015
+ { type: "uint256" },
1016
+ // timestamp
1017
+ { type: "uint256" },
1018
+ // netTotalMessageLength
1019
+ { type: "uint256" },
1020
+ // netTotalMessageForAppTopicLength
1021
+ {
1022
+ name: "zoneParameters",
1023
+ type: "tuple",
1024
+ internalType: "struct ZoneParameters",
1025
+ components: [
1026
+ { name: "orderHash", type: "bytes32", internalType: "bytes32" },
1027
+ { name: "fulfiller", type: "address", internalType: "address" },
1028
+ { name: "offerer", type: "address", internalType: "address" },
1029
+ {
1030
+ name: "offer",
1031
+ type: "tuple[]",
1032
+ internalType: "struct SpentItem[]",
1033
+ components: [
1034
+ { name: "itemType", type: "uint8", internalType: "enum ItemType" },
1035
+ { name: "token", type: "address", internalType: "address" },
1036
+ { name: "identifier", type: "uint256", internalType: "uint256" },
1037
+ { name: "amount", type: "uint256", internalType: "uint256" }
1038
+ ]
1039
+ },
1040
+ {
1041
+ name: "consideration",
1042
+ type: "tuple[]",
1043
+ internalType: "struct ReceivedItem[]",
1044
+ components: [
1045
+ { name: "itemType", type: "uint8", internalType: "enum ItemType" },
1046
+ { name: "token", type: "address", internalType: "address" },
1047
+ { name: "identifier", type: "uint256", internalType: "uint256" },
1048
+ { name: "amount", type: "uint256", internalType: "uint256" },
1049
+ { name: "recipient", type: "address", internalType: "address payable" }
1050
+ ]
1051
+ },
1052
+ { name: "extraData", type: "bytes", internalType: "bytes" },
1053
+ { name: "orderHashes", type: "bytes32[]", internalType: "bytes32[]" },
1054
+ { name: "startTime", type: "uint256", internalType: "uint256" },
1055
+ { name: "endTime", type: "uint256", internalType: "uint256" },
1056
+ { name: "zoneHash", type: "bytes32", internalType: "bytes32" }
1057
+ ]
1058
+ }
1059
+ ];
1060
+ function parseSaleFromStoredData(storedData, chainId) {
1061
+ try {
1062
+ const cleanedData = "0x" + (storedData.startsWith("0x") ? storedData.slice(2) : storedData);
1063
+ const [timestamp, , , zoneParameters] = decodeAbiParameters(
1064
+ ZONE_STORED_SALE_ABI,
1065
+ cleanedData
1066
+ );
1067
+ const offerItem = zoneParameters.offer[0];
1068
+ if (!offerItem) return null;
1069
+ const totalConsideration = zoneParameters.consideration.reduce(
1070
+ (acc, item) => acc + item.amount,
1071
+ BigInt(0)
1072
+ );
1073
+ return {
1074
+ seller: zoneParameters.offerer,
1075
+ buyer: zoneParameters.fulfiller,
1076
+ tokenAddress: offerItem.token,
1077
+ tokenId: offerItem.identifier.toString(),
1078
+ amount: offerItem.amount,
1079
+ itemType: offerItem.itemType,
1080
+ priceWei: totalConsideration,
1081
+ price: parseFloat(formatEther(totalConsideration)),
1082
+ currency: getCurrencySymbol(chainId),
1083
+ timestamp: Number(timestamp),
1084
+ orderHash: zoneParameters.orderHash
1085
+ };
1086
+ } catch {
1087
+ return null;
1088
+ }
1089
+ }
1090
+ function sortSalesByTimestamp(sales) {
1091
+ return [...sales].sort((a, b) => b.timestamp - a.timestamp);
1092
+ }
1017
1093
  var CHAIN_RPC_URLS = {
1018
1094
  8453: ["https://base-mainnet.public.blastapi.io", "https://mainnet.base.org"],
1019
1095
  84532: ["https://sepolia.base.org"],
@@ -1076,7 +1152,7 @@ var BazaarClient = class {
1076
1152
  const bazaarAddress = getBazaarAddress(this.chainId);
1077
1153
  const filter = {
1078
1154
  appAddress: bazaarAddress,
1079
- topic: nftAddress.toLowerCase(),
1155
+ topic: nftAddress?.toLowerCase(),
1080
1156
  maker
1081
1157
  };
1082
1158
  let startIndex;
@@ -1149,18 +1225,57 @@ var BazaarClient = class {
1149
1225
  }
1150
1226
  const openListings = listings.filter((l) => l.orderStatus === 2 /* OPEN */);
1151
1227
  const expiredListings = includeExpired ? listings.filter((l) => l.orderStatus === 1 /* EXPIRED */) : [];
1152
- const tokenIds = openListings.map((l) => l.tokenId);
1153
- const owners = await bulkFetchNftOwners(this.client, nftAddress, tokenIds);
1228
+ let validOpenListings;
1154
1229
  const beforeOwnership = openListings.length;
1155
- const validOpenListings = openListings.filter((listing, index) => {
1156
- const owner = owners[index];
1157
- return isListingValid(
1158
- listing.orderStatus,
1159
- listing.expirationDate,
1160
- listing.maker,
1161
- owner
1230
+ if (nftAddress) {
1231
+ const tokenIds = openListings.map((l) => l.tokenId);
1232
+ const owners = await bulkFetchNftOwners(this.client, nftAddress, tokenIds);
1233
+ validOpenListings = openListings.filter((listing, index) => {
1234
+ const owner = owners[index];
1235
+ return isListingValid(
1236
+ listing.orderStatus,
1237
+ listing.expirationDate,
1238
+ listing.maker,
1239
+ owner
1240
+ );
1241
+ });
1242
+ } else {
1243
+ const groups = /* @__PURE__ */ new Map();
1244
+ for (const listing of openListings) {
1245
+ const key = listing.nftAddress.toLowerCase();
1246
+ const group = groups.get(key);
1247
+ if (group) {
1248
+ group.push(listing);
1249
+ } else {
1250
+ groups.set(key, [listing]);
1251
+ }
1252
+ }
1253
+ const groupEntries = Array.from(groups.entries());
1254
+ const ownerResults = await Promise.all(
1255
+ groupEntries.map(
1256
+ ([addr, groupListings]) => bulkFetchNftOwners(
1257
+ this.client,
1258
+ addr,
1259
+ groupListings.map((l) => l.tokenId)
1260
+ )
1261
+ )
1162
1262
  );
1163
- });
1263
+ validOpenListings = [];
1264
+ groupEntries.forEach(([, groupListings], groupIndex) => {
1265
+ const owners = ownerResults[groupIndex];
1266
+ for (let i = 0; i < groupListings.length; i++) {
1267
+ const listing = groupListings[i];
1268
+ if (isListingValid(
1269
+ listing.orderStatus,
1270
+ listing.expirationDate,
1271
+ listing.maker,
1272
+ owners[i]
1273
+ )) {
1274
+ validOpenListings.push(listing);
1275
+ }
1276
+ }
1277
+ });
1278
+ }
1164
1279
  console.log(tag, `after ownership filter: ${validOpenListings.length}/${beforeOwnership} (${beforeOwnership - validOpenListings.length} dropped)`);
1165
1280
  const dedupedOpen = sortListingsByPrice(getBestListingPerToken(validOpenListings));
1166
1281
  const activeTokenKeys = new Set(dedupedOpen.map((l) => `${l.nftAddress.toLowerCase()}-${l.tokenId}`));
@@ -1493,6 +1608,72 @@ var BazaarClient = class {
1493
1608
  console.log(tag, `after balance filter: ${listings.length}/${beforeBalance} (${beforeBalance - listings.length} dropped)`);
1494
1609
  return sortErc20ListingsByPricePerToken(listings);
1495
1610
  }
1611
+ /**
1612
+ * Get recent sales for a collection
1613
+ *
1614
+ * Sales are stored differently from listings: Net messages contain order hashes,
1615
+ * and the actual sale data is fetched from the bulk storage contract.
1616
+ *
1617
+ * Results are sorted by timestamp (most recent first)
1618
+ */
1619
+ async getSales(options) {
1620
+ const { nftAddress, maxMessages = 100 } = options;
1621
+ const filter = {
1622
+ appAddress: NET_SEAPORT_ZONE_ADDRESS,
1623
+ topic: nftAddress.toLowerCase()
1624
+ };
1625
+ const count = await this.netClient.getMessageCount({ filter });
1626
+ if (count === 0) {
1627
+ return [];
1628
+ }
1629
+ const startIndex = Math.max(0, count - maxMessages);
1630
+ const messages = await this.netClient.getMessages({
1631
+ filter,
1632
+ startIndex,
1633
+ endIndex: count
1634
+ });
1635
+ return this.processSalesFromMessages(messages, options);
1636
+ }
1637
+ /**
1638
+ * Process pre-fetched messages into sales.
1639
+ *
1640
+ * Each message's data field contains an order hash. The actual sale data
1641
+ * is fetched from the bulk storage contract using these order hashes.
1642
+ */
1643
+ async processSalesFromMessages(messages, options) {
1644
+ const tag = `[BazaarClient.processSales chain=${this.chainId}]`;
1645
+ if (messages.length === 0) {
1646
+ return [];
1647
+ }
1648
+ const orderHashes = messages.map((m) => m.data);
1649
+ console.log(tag, `fetching ${orderHashes.length} sale details from storage...`);
1650
+ const bulkKeys = orderHashes.map((hash) => ({
1651
+ key: hash,
1652
+ operator: NET_SEAPORT_ZONE_ADDRESS
1653
+ }));
1654
+ let storedResults;
1655
+ try {
1656
+ storedResults = await this.client.readContract({
1657
+ abi: STORAGE_CONTRACT.abi,
1658
+ address: STORAGE_CONTRACT.address,
1659
+ functionName: "bulkGet",
1660
+ args: [bulkKeys]
1661
+ });
1662
+ } catch (err) {
1663
+ console.error(tag, "bulk storage fetch failed:", err);
1664
+ return [];
1665
+ }
1666
+ const sales = [];
1667
+ for (const result of storedResults) {
1668
+ if (!result.value || result.value === "0x") continue;
1669
+ const sale = parseSaleFromStoredData(result.value, this.chainId);
1670
+ if (sale) {
1671
+ sales.push(sale);
1672
+ }
1673
+ }
1674
+ console.log(tag, `parsed ${sales.length}/${storedResults.length} sales`);
1675
+ return sales;
1676
+ }
1496
1677
  /**
1497
1678
  * Get the chain ID this client is configured for
1498
1679
  */
@@ -1608,6 +1789,6 @@ var BazaarClient = class {
1608
1789
  }
1609
1790
  };
1610
1791
 
1611
- export { BAZAAR_COLLECTION_OFFERS_ABI, BAZAAR_SUBMISSION_ABI, BAZAAR_V2_ABI, BULK_SEAPORT_ORDER_STATUS_FETCHER_ABI, BULK_SEAPORT_ORDER_STATUS_FETCHER_ADDRESS, BazaarClient, ERC20_BULK_BALANCE_CHECKER_ABI, ERC20_BULK_BALANCE_CHECKER_ADDRESS, ERC721_OWNER_OF_HELPER_ABI, ERC721_OWNER_OF_HELPER_ADDRESS, ItemType, NET_SEAPORT_COLLECTION_OFFER_ZONE_ADDRESS, NET_SEAPORT_PRIVATE_ORDER_ZONE_ADDRESS, NET_SEAPORT_ZONE_ADDRESS, OrderType, SEAPORT_CANCEL_ABI, SeaportOrderStatus, bulkFetchErc20Balances, bulkFetchNftOwners, bulkFetchOrderStatuses, computeOrderHash, createBalanceMap, createOrderStatusMap, createOwnershipMap, createSeaportInstance, decodeSeaportSubmission, formatPrice, getBazaarAddress, getBazaarChainConfig, getBazaarSupportedChainIds, getBestCollectionOffer, getBestListingPerToken, getCollectionOffersAddress, getCurrencySymbol, getErc20BazaarAddress, getErc20OffersAddress, getFeeCollectorAddress, getHighEthAddress, getNftFeeBps, getOrderStatusFromInfo, getSeaportAddress, getSeaportOrderFromMessageData, getTotalConsiderationAmount, getWrappedNativeCurrency, isBazaarSupportedOnChain, isCollectionOfferValid, isErc20ListingValid, isErc20OfferValid, isListingValid, parseCollectionOfferFromMessage, parseErc20ListingFromMessage, parseErc20OfferFromMessage, parseListingFromMessage, sortErc20ListingsByPricePerToken, sortErc20OffersByPricePerToken, sortListingsByPrice, sortOffersByPrice };
1792
+ export { BAZAAR_COLLECTION_OFFERS_ABI, BAZAAR_SUBMISSION_ABI, BAZAAR_V2_ABI, BULK_SEAPORT_ORDER_STATUS_FETCHER_ABI, BULK_SEAPORT_ORDER_STATUS_FETCHER_ADDRESS, BazaarClient, ERC20_BULK_BALANCE_CHECKER_ABI, ERC20_BULK_BALANCE_CHECKER_ADDRESS, ERC721_OWNER_OF_HELPER_ABI, ERC721_OWNER_OF_HELPER_ADDRESS, ItemType, NET_SEAPORT_COLLECTION_OFFER_ZONE_ADDRESS, NET_SEAPORT_PRIVATE_ORDER_ZONE_ADDRESS, NET_SEAPORT_ZONE_ADDRESS, OrderType, SEAPORT_CANCEL_ABI, SeaportOrderStatus, bulkFetchErc20Balances, bulkFetchNftOwners, bulkFetchOrderStatuses, computeOrderHash, createBalanceMap, createOrderStatusMap, createOwnershipMap, createSeaportInstance, decodeSeaportSubmission, formatPrice, getBazaarAddress, getBazaarChainConfig, getBazaarSupportedChainIds, getBestCollectionOffer, getBestListingPerToken, getCollectionOffersAddress, getCurrencySymbol, getErc20BazaarAddress, getErc20OffersAddress, getFeeCollectorAddress, getHighEthAddress, getNftFeeBps, getOrderStatusFromInfo, getSeaportAddress, getSeaportOrderFromMessageData, getTotalConsiderationAmount, getWrappedNativeCurrency, isBazaarSupportedOnChain, isCollectionOfferValid, isErc20ListingValid, isErc20OfferValid, isListingValid, parseCollectionOfferFromMessage, parseErc20ListingFromMessage, parseErc20OfferFromMessage, parseListingFromMessage, parseSaleFromStoredData, sortErc20ListingsByPricePerToken, sortErc20OffersByPricePerToken, sortListingsByPrice, sortOffersByPrice, sortSalesByTimestamp };
1612
1793
  //# sourceMappingURL=index.mjs.map
1613
1794
  //# sourceMappingURL=index.mjs.map