@net-protocol/bazaar 0.1.0 → 0.1.2

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
@@ -177,17 +177,20 @@ var DEFAULT_SEAPORT_ADDRESS = "0x0000000000000068F116a894984e2DB1123eB395";
177
177
  var DEFAULT_BAZAAR_ADDRESS = "0x00000000E3dA5fC031282A39759bDDA78ae7fAE5";
178
178
  var DEFAULT_COLLECTION_OFFERS_ADDRESS = "0x0000000D43423E0A12CecB307a74591999b32B32";
179
179
  var DEFAULT_FEE_COLLECTOR_ADDRESS = "0x32D16C15410248bef498D7aF50D10Db1a546b9E5";
180
+ var DEFAULT_ERC20_BAZAAR_ADDRESS = "0x00000000a2d173a4610c85c7471a25b6bc216a70";
180
181
  var DEFAULT_NFT_FEE_BPS = 500;
181
182
  var BULK_SEAPORT_ORDER_STATUS_FETCHER_ADDRESS = "0x0000009112ABCE652674b4fE3eD9C765B22d11A7";
182
183
  var ERC721_OWNER_OF_HELPER_ADDRESS = "0x000000aa4eFa2e5A4a6002C7F08B6e8Ec8cf1dDa";
183
184
  var ERC20_BULK_BALANCE_CHECKER_ADDRESS = "0x000000b50a9f2923f2db931391824f6d1278f712";
184
185
  var NET_SEAPORT_COLLECTION_OFFER_ZONE_ADDRESS = "0x000000B799ec6D7aCC1B578f62bFc324c25DFC5A";
186
+ var NET_SEAPORT_PRIVATE_ORDER_ZONE_ADDRESS = "0x000000bC63761cbb05305632212e2f3AE2BE7a9B";
185
187
  var BAZAAR_CHAIN_CONFIGS = {
186
188
  // Base Mainnet
187
189
  8453: {
188
190
  bazaarAddress: "0x000000058f3ade587388daf827174d0e6fc97595",
189
191
  collectionOffersAddress: "0x0000000f9c45efcff0f78d8b54aa6a40092d66dc",
190
192
  erc20OffersAddress: "0x0000000e23a89aa06f317306aa1ae231d3503082",
193
+ erc20BazaarAddress: "0x00000006557e3629e2fc50bbad0c002b27cac492",
191
194
  seaportAddress: DEFAULT_SEAPORT_ADDRESS,
192
195
  feeCollectorAddress: "0x66547ff4f7206e291F7BC157b54C026Fc6660961",
193
196
  nftFeeBps: 0,
@@ -277,6 +280,7 @@ var BAZAAR_CHAIN_CONFIGS = {
277
280
  bazaarAddress: "0x000000058f3ade587388daf827174d0e6fc97595",
278
281
  collectionOffersAddress: "0x0000000f9c45efcff0f78d8b54aa6a40092d66dc",
279
282
  erc20OffersAddress: "0x0000000e23a89aa06f317306aa1ae231d3503082",
283
+ erc20BazaarAddress: "0x00000006557e3629e2fc50bbad0c002b27cac492",
280
284
  seaportAddress: DEFAULT_SEAPORT_ADDRESS,
281
285
  feeCollectorAddress: "0x66547ff4f7206e291F7BC157b54C026Fc6660961",
282
286
  nftFeeBps: 0,
@@ -344,6 +348,9 @@ function getHighEthAddress(chainId) {
344
348
  function getErc20OffersAddress(chainId) {
345
349
  return BAZAAR_CHAIN_CONFIGS[chainId]?.erc20OffersAddress;
346
350
  }
351
+ function getErc20BazaarAddress(chainId) {
352
+ return BAZAAR_CHAIN_CONFIGS[chainId]?.erc20BazaarAddress ?? DEFAULT_ERC20_BAZAAR_ADDRESS;
353
+ }
347
354
  function decodeSeaportSubmission(messageData) {
348
355
  const [decoded] = decodeAbiParameters(BAZAAR_SUBMISSION_ABI, messageData);
349
356
  return {
@@ -541,6 +548,19 @@ function isErc20OfferValid(orderStatus, expirationDate, priceWei, buyerWethBalan
541
548
  }
542
549
  return true;
543
550
  }
551
+ function isErc20ListingValid(orderStatus, expirationDate, tokenAmount, sellerTokenBalance) {
552
+ if (orderStatus !== 2 /* OPEN */) {
553
+ return false;
554
+ }
555
+ const now = Math.floor(Date.now() / 1e3);
556
+ if (expirationDate <= now) {
557
+ return false;
558
+ }
559
+ if (sellerTokenBalance < tokenAmount) {
560
+ return false;
561
+ }
562
+ return true;
563
+ }
544
564
 
545
565
  // src/utils/parsing.ts
546
566
  function parseListingFromMessage(message, chainId) {
@@ -556,6 +576,7 @@ function parseListingFromMessage(message, chainId) {
556
576
  }
557
577
  const priceWei = getTotalConsiderationAmount(parameters);
558
578
  const tokenId = offerItem.identifierOrCriteria.toString();
579
+ const targetFulfiller = parameters.zone.toLowerCase() === NET_SEAPORT_PRIVATE_ORDER_ZONE_ADDRESS.toLowerCase() && parameters.zoneHash !== "0x0000000000000000000000000000000000000000000000000000000000000000" ? parameters.zoneHash : void 0;
559
580
  return {
560
581
  maker: parameters.offerer,
561
582
  nftAddress: offerItem.token,
@@ -572,7 +593,8 @@ function parseListingFromMessage(message, chainId) {
572
593
  orderComponents: {
573
594
  ...parameters,
574
595
  counter: submission.counter
575
- }
596
+ },
597
+ targetFulfiller
576
598
  };
577
599
  } catch {
578
600
  return null;
@@ -702,6 +724,58 @@ function sortErc20OffersByPricePerToken(offers) {
702
724
  return 0;
703
725
  });
704
726
  }
727
+ function parseErc20ListingFromMessage(message, chainId) {
728
+ try {
729
+ const submission = decodeSeaportSubmission(message.data);
730
+ const { parameters } = submission;
731
+ if (parameters.zone.toLowerCase() === NET_SEAPORT_COLLECTION_OFFER_ZONE_ADDRESS.toLowerCase()) {
732
+ return null;
733
+ }
734
+ const offerItem = parameters.offer[0];
735
+ if (!offerItem || offerItem.itemType !== 1 /* ERC20 */) {
736
+ return null;
737
+ }
738
+ const tokenAmount = offerItem.startAmount;
739
+ if (tokenAmount === BigInt(0)) {
740
+ return null;
741
+ }
742
+ const priceWei = getTotalConsiderationAmount(parameters);
743
+ if (priceWei === BigInt(0)) {
744
+ return null;
745
+ }
746
+ const pricePerTokenWei = priceWei / tokenAmount;
747
+ return {
748
+ maker: parameters.offerer,
749
+ tokenAddress: offerItem.token,
750
+ tokenAmount,
751
+ priceWei,
752
+ pricePerTokenWei,
753
+ price: formatPrice(priceWei),
754
+ pricePerToken: formatPrice(pricePerTokenWei),
755
+ currency: getCurrencySymbol(chainId),
756
+ expirationDate: Number(parameters.endTime),
757
+ orderHash: "0x",
758
+ // Will be computed later
759
+ orderStatus: 2 /* OPEN */,
760
+ // Will be validated later
761
+ messageData: message.data,
762
+ orderComponents: {
763
+ ...parameters,
764
+ counter: submission.counter
765
+ }
766
+ };
767
+ } catch {
768
+ return null;
769
+ }
770
+ }
771
+ function sortErc20ListingsByPricePerToken(listings) {
772
+ return [...listings].sort((a, b) => {
773
+ const diff = a.pricePerTokenWei - b.pricePerTokenWei;
774
+ if (diff < BigInt(0)) return -1;
775
+ if (diff > BigInt(0)) return 1;
776
+ return 0;
777
+ });
778
+ }
705
779
 
706
780
  // src/client/BazaarClient.ts
707
781
  var CHAIN_RPC_URLS = {
@@ -758,25 +832,30 @@ var BazaarClient = class {
758
832
  * Results are deduplicated (one per token) and sorted by price (lowest first)
759
833
  */
760
834
  async getListings(options) {
761
- const { nftAddress, excludeMaker, maxMessages = 200 } = options;
835
+ const { nftAddress, excludeMaker, maker, maxMessages = 200 } = options;
762
836
  const bazaarAddress = getBazaarAddress(this.chainId);
763
- const count = await this.netClient.getMessageCount({
764
- filter: {
765
- appAddress: bazaarAddress,
766
- topic: nftAddress.toLowerCase()
837
+ const filter = {
838
+ appAddress: bazaarAddress,
839
+ topic: nftAddress.toLowerCase(),
840
+ maker
841
+ };
842
+ let startIndex;
843
+ let endIndex;
844
+ if (options.startIndex != null && options.endIndex != null) {
845
+ startIndex = options.startIndex;
846
+ endIndex = options.endIndex;
847
+ } else {
848
+ const count = await this.netClient.getMessageCount({ filter });
849
+ if (count === 0) {
850
+ return [];
767
851
  }
768
- });
769
- if (count === 0) {
770
- return [];
852
+ startIndex = Math.max(0, count - maxMessages);
853
+ endIndex = count;
771
854
  }
772
- const startIndex = Math.max(0, count - maxMessages);
773
855
  const messages = await this.netClient.getMessages({
774
- filter: {
775
- appAddress: bazaarAddress,
776
- topic: nftAddress.toLowerCase()
777
- },
856
+ filter,
778
857
  startIndex,
779
- endIndex: count
858
+ endIndex
780
859
  });
781
860
  let listings = [];
782
861
  for (const message of messages) {
@@ -997,6 +1076,92 @@ var BazaarClient = class {
997
1076
  });
998
1077
  return sortErc20OffersByPricePerToken(offers);
999
1078
  }
1079
+ /**
1080
+ * Get valid ERC20 listings for a token
1081
+ *
1082
+ * Returns listings that are:
1083
+ * - OPEN status (not filled, cancelled, or expired)
1084
+ * - Not expired
1085
+ * - Seller has sufficient ERC20 token balance
1086
+ *
1087
+ * Results are sorted by price per token (lowest first). No deduplication —
1088
+ * all valid listings are returned (grouped by maker in the UI).
1089
+ */
1090
+ async getErc20Listings(options) {
1091
+ const { tokenAddress, excludeMaker, maker, maxMessages = 200 } = options;
1092
+ const erc20BazaarAddress = getErc20BazaarAddress(this.chainId);
1093
+ const filter = {
1094
+ appAddress: erc20BazaarAddress,
1095
+ topic: tokenAddress.toLowerCase(),
1096
+ maker
1097
+ };
1098
+ let startIndex;
1099
+ let endIndex;
1100
+ if (options.startIndex != null && options.endIndex != null) {
1101
+ startIndex = options.startIndex;
1102
+ endIndex = options.endIndex;
1103
+ } else {
1104
+ const count = await this.netClient.getMessageCount({ filter });
1105
+ if (count === 0) {
1106
+ return [];
1107
+ }
1108
+ startIndex = Math.max(0, count - maxMessages);
1109
+ endIndex = count;
1110
+ }
1111
+ const messages = await this.netClient.getMessages({
1112
+ filter,
1113
+ startIndex,
1114
+ endIndex
1115
+ });
1116
+ let listings = [];
1117
+ for (const message of messages) {
1118
+ const listing = parseErc20ListingFromMessage(message, this.chainId);
1119
+ if (!listing) continue;
1120
+ if (listing.tokenAddress.toLowerCase() !== tokenAddress.toLowerCase()) {
1121
+ continue;
1122
+ }
1123
+ if (excludeMaker && listing.maker.toLowerCase() === excludeMaker.toLowerCase()) {
1124
+ continue;
1125
+ }
1126
+ listings.push(listing);
1127
+ }
1128
+ if (listings.length === 0) {
1129
+ return [];
1130
+ }
1131
+ const seaport = createSeaportInstance(this.chainId, this.rpcUrl);
1132
+ for (const listing of listings) {
1133
+ const order = getSeaportOrderFromMessageData(listing.messageData);
1134
+ listing.orderHash = computeOrderHash(seaport, order.parameters, order.counter);
1135
+ }
1136
+ const orderHashes = listings.map((l) => l.orderHash);
1137
+ const statusInfos = await bulkFetchOrderStatuses(this.client, this.chainId, orderHashes);
1138
+ listings.forEach((listing, index) => {
1139
+ const statusInfo = statusInfos[index];
1140
+ listing.orderStatus = getOrderStatusFromInfo(listing.orderComponents, statusInfo);
1141
+ });
1142
+ listings = listings.filter(
1143
+ (l) => l.orderStatus === 2 /* OPEN */ && l.expirationDate > Math.floor(Date.now() / 1e3)
1144
+ );
1145
+ if (listings.length === 0) {
1146
+ return [];
1147
+ }
1148
+ const uniqueMakers = [...new Set(listings.map((l) => l.maker))];
1149
+ const balances = await bulkFetchErc20Balances(this.client, tokenAddress, uniqueMakers);
1150
+ const balanceMap = /* @__PURE__ */ new Map();
1151
+ uniqueMakers.forEach((maker2, index) => {
1152
+ balanceMap.set(maker2.toLowerCase(), balances[index]);
1153
+ });
1154
+ listings = listings.filter((listing) => {
1155
+ const balance = balanceMap.get(listing.maker.toLowerCase()) || BigInt(0);
1156
+ return isErc20ListingValid(
1157
+ listing.orderStatus,
1158
+ listing.expirationDate,
1159
+ listing.tokenAmount,
1160
+ balance
1161
+ );
1162
+ });
1163
+ return sortErc20ListingsByPricePerToken(listings);
1164
+ }
1000
1165
  /**
1001
1166
  * Get the chain ID this client is configured for
1002
1167
  */
@@ -1022,6 +1187,12 @@ var BazaarClient = class {
1022
1187
  getErc20OffersAddress() {
1023
1188
  return getErc20OffersAddress(this.chainId);
1024
1189
  }
1190
+ /**
1191
+ * Get the ERC20 bazaar (listings) contract address for this chain
1192
+ */
1193
+ getErc20BazaarAddress() {
1194
+ return getErc20BazaarAddress(this.chainId);
1195
+ }
1025
1196
  /**
1026
1197
  * Get the Seaport contract address for this chain
1027
1198
  */
@@ -1052,6 +1223,18 @@ var BazaarClient = class {
1052
1223
  }
1053
1224
  return this.prepareCancelOrder(offer.orderComponents);
1054
1225
  }
1226
+ /**
1227
+ * Prepare a transaction to cancel an ERC20 listing
1228
+ *
1229
+ * The listing must have been created by the caller.
1230
+ * Use the orderComponents from the Erc20Listing object returned by getErc20Listings().
1231
+ */
1232
+ prepareCancelErc20Listing(listing) {
1233
+ if (!listing.orderComponents) {
1234
+ throw new Error("Listing does not have order components");
1235
+ }
1236
+ return this.prepareCancelOrder(listing.orderComponents);
1237
+ }
1055
1238
  /**
1056
1239
  * Prepare a transaction to cancel a Seaport order
1057
1240
  *
@@ -1099,13 +1282,17 @@ function useBazaarListings({
1099
1282
  chainId,
1100
1283
  nftAddress,
1101
1284
  excludeMaker,
1285
+ maker,
1102
1286
  maxMessages = 200,
1287
+ startIndex: startIndexOverride,
1288
+ endIndex: endIndexOverride,
1103
1289
  enabled = true
1104
1290
  }) {
1105
1291
  const [listings, setListings] = useState([]);
1106
1292
  const [isProcessing, setIsProcessing] = useState(false);
1107
1293
  const [processingError, setProcessingError] = useState();
1108
1294
  const [refetchTrigger, setRefetchTrigger] = useState(0);
1295
+ const hasRangeOverride = startIndexOverride != null && endIndexOverride != null;
1109
1296
  const isSupported = useMemo(
1110
1297
  () => isBazaarSupportedOnChain(chainId),
1111
1298
  [chainId]
@@ -1117,19 +1304,18 @@ function useBazaarListings({
1117
1304
  const filter = useMemo(
1118
1305
  () => ({
1119
1306
  appAddress: bazaarAddress,
1120
- topic: nftAddress.toLowerCase()
1307
+ topic: nftAddress.toLowerCase(),
1308
+ maker
1121
1309
  }),
1122
- [bazaarAddress, nftAddress]
1310
+ [bazaarAddress, nftAddress, maker]
1123
1311
  );
1124
1312
  const { count: totalCount, isLoading: isLoadingCount } = useNetMessageCount({
1125
1313
  chainId,
1126
1314
  filter,
1127
- enabled: enabled && isSupported
1315
+ enabled: enabled && isSupported && !hasRangeOverride
1128
1316
  });
1129
- const startIndex = useMemo(
1130
- () => Math.max(0, totalCount - maxMessages),
1131
- [totalCount, maxMessages]
1132
- );
1317
+ const startIndex = hasRangeOverride ? startIndexOverride : Math.max(0, totalCount - maxMessages);
1318
+ const endIndex = hasRangeOverride ? endIndexOverride : totalCount;
1133
1319
  const {
1134
1320
  messages,
1135
1321
  isLoading: isLoadingMessages,
@@ -1139,8 +1325,8 @@ function useBazaarListings({
1139
1325
  chainId,
1140
1326
  filter,
1141
1327
  startIndex,
1142
- endIndex: totalCount,
1143
- enabled: enabled && isSupported && totalCount > 0
1328
+ endIndex,
1329
+ enabled: enabled && isSupported && (hasRangeOverride || totalCount > 0)
1144
1330
  });
1145
1331
  useEffect(() => {
1146
1332
  if (!isSupported || !enabled) {
@@ -1160,7 +1346,10 @@ function useBazaarListings({
1160
1346
  const validListings = await client.getListings({
1161
1347
  nftAddress,
1162
1348
  excludeMaker,
1163
- maxMessages
1349
+ maker,
1350
+ maxMessages,
1351
+ startIndex: hasRangeOverride ? startIndexOverride : void 0,
1352
+ endIndex: hasRangeOverride ? endIndexOverride : void 0
1164
1353
  });
1165
1354
  if (!cancelled) {
1166
1355
  setListings(validListings);
@@ -1180,14 +1369,14 @@ function useBazaarListings({
1180
1369
  return () => {
1181
1370
  cancelled = true;
1182
1371
  };
1183
- }, [chainId, nftAddress, excludeMaker, maxMessages, messages, isSupported, enabled, refetchTrigger]);
1372
+ }, [chainId, nftAddress, excludeMaker, maker, maxMessages, startIndexOverride, endIndexOverride, hasRangeOverride, messages, isSupported, enabled, refetchTrigger]);
1184
1373
  const refetch = () => {
1185
1374
  refetchMessages();
1186
1375
  setRefetchTrigger((t) => t + 1);
1187
1376
  };
1188
1377
  return {
1189
1378
  listings,
1190
- isLoading: isLoadingCount || isLoadingMessages || isProcessing,
1379
+ isLoading: (hasRangeOverride ? false : isLoadingCount) || isLoadingMessages || isProcessing,
1191
1380
  error: messagesError || processingError,
1192
1381
  refetch
1193
1382
  };
@@ -1387,7 +1576,110 @@ function useBazaarErc20Offers({
1387
1576
  refetch
1388
1577
  };
1389
1578
  }
1579
+ function useBazaarErc20Listings({
1580
+ chainId,
1581
+ tokenAddress,
1582
+ excludeMaker,
1583
+ maker,
1584
+ maxMessages = 200,
1585
+ startIndex: startIndexOverride,
1586
+ endIndex: endIndexOverride,
1587
+ enabled = true
1588
+ }) {
1589
+ const [listings, setListings] = useState([]);
1590
+ const [isProcessing, setIsProcessing] = useState(false);
1591
+ const [processingError, setProcessingError] = useState();
1592
+ const [refetchTrigger, setRefetchTrigger] = useState(0);
1593
+ const hasRangeOverride = startIndexOverride != null && endIndexOverride != null;
1594
+ const isSupported = useMemo(
1595
+ () => isBazaarSupportedOnChain(chainId),
1596
+ [chainId]
1597
+ );
1598
+ const erc20BazaarAddress = useMemo(
1599
+ () => isSupported ? getErc20BazaarAddress(chainId) : void 0,
1600
+ [chainId, isSupported]
1601
+ );
1602
+ const filter = useMemo(
1603
+ () => ({
1604
+ appAddress: erc20BazaarAddress,
1605
+ topic: tokenAddress.toLowerCase(),
1606
+ maker
1607
+ }),
1608
+ [erc20BazaarAddress, tokenAddress, maker]
1609
+ );
1610
+ const { count: totalCount, isLoading: isLoadingCount } = useNetMessageCount({
1611
+ chainId,
1612
+ filter,
1613
+ enabled: enabled && isSupported && !hasRangeOverride
1614
+ });
1615
+ const startIndex = hasRangeOverride ? startIndexOverride : Math.max(0, totalCount - maxMessages);
1616
+ const endIndex = hasRangeOverride ? endIndexOverride : totalCount;
1617
+ const {
1618
+ messages,
1619
+ isLoading: isLoadingMessages,
1620
+ error: messagesError,
1621
+ refetch: refetchMessages
1622
+ } = useNetMessages({
1623
+ chainId,
1624
+ filter,
1625
+ startIndex,
1626
+ endIndex,
1627
+ enabled: enabled && isSupported && (hasRangeOverride || totalCount > 0)
1628
+ });
1629
+ useEffect(() => {
1630
+ if (!isSupported || !enabled) {
1631
+ setListings([]);
1632
+ return;
1633
+ }
1634
+ if (!messages || messages.length === 0) {
1635
+ setListings([]);
1636
+ return;
1637
+ }
1638
+ let cancelled = false;
1639
+ async function processListings() {
1640
+ setIsProcessing(true);
1641
+ setProcessingError(void 0);
1642
+ try {
1643
+ const client = new BazaarClient({ chainId });
1644
+ const validListings = await client.getErc20Listings({
1645
+ tokenAddress,
1646
+ excludeMaker,
1647
+ maker,
1648
+ maxMessages,
1649
+ startIndex: hasRangeOverride ? startIndexOverride : void 0,
1650
+ endIndex: hasRangeOverride ? endIndexOverride : void 0
1651
+ });
1652
+ if (!cancelled) {
1653
+ setListings(validListings);
1654
+ }
1655
+ } catch (err) {
1656
+ if (!cancelled) {
1657
+ setProcessingError(err instanceof Error ? err : new Error(String(err)));
1658
+ setListings([]);
1659
+ }
1660
+ } finally {
1661
+ if (!cancelled) {
1662
+ setIsProcessing(false);
1663
+ }
1664
+ }
1665
+ }
1666
+ processListings();
1667
+ return () => {
1668
+ cancelled = true;
1669
+ };
1670
+ }, [chainId, tokenAddress, excludeMaker, maker, maxMessages, startIndexOverride, endIndexOverride, hasRangeOverride, messages, isSupported, enabled, refetchTrigger]);
1671
+ const refetch = () => {
1672
+ refetchMessages();
1673
+ setRefetchTrigger((t) => t + 1);
1674
+ };
1675
+ return {
1676
+ listings,
1677
+ isLoading: (hasRangeOverride ? false : isLoadingCount) || isLoadingMessages || isProcessing,
1678
+ error: messagesError || processingError,
1679
+ refetch
1680
+ };
1681
+ }
1390
1682
 
1391
- export { useBazaarCollectionOffers, useBazaarErc20Offers, useBazaarListings };
1683
+ export { useBazaarCollectionOffers, useBazaarErc20Listings, useBazaarErc20Offers, useBazaarListings };
1392
1684
  //# sourceMappingURL=react.mjs.map
1393
1685
  //# sourceMappingURL=react.mjs.map