diff --git a/contracts/interfaces/handlers/IBosonAccountHandler.sol b/contracts/interfaces/handlers/IBosonAccountHandler.sol index 65a01182d..738082b82 100644 --- a/contracts/interfaces/handlers/IBosonAccountHandler.sol +++ b/contracts/interfaces/handlers/IBosonAccountHandler.sol @@ -10,7 +10,7 @@ import { IBosonAccountEvents } from "../events/IBosonAccountEvents.sol"; * * @notice Handles creation, update, retrieval of accounts within the protocol. * - * The ERC-165 identifier for this interface is: 0x890d5d20 + * The ERC-165 identifier for this interface is: 0x079a9d3b */ interface IBosonAccountHandler is IBosonAccountEvents, BosonErrors { /** @@ -450,7 +450,8 @@ interface IBosonAccountHandler is IBosonAccountEvents, BosonErrors { ) external view returns (bool exists, BosonTypes.Seller memory seller, BosonTypes.AuthToken memory authToken); /** - * @notice Gets the details about a seller's collections. + * @notice Gets the details about all seller's collections. + * In case seller has too many collections and this runs out of gas, please use getSellersCollectionsPaginated. * * @param _sellerId - the id of the seller to check * @return defaultVoucherAddress - the address of the default voucher contract for the seller @@ -460,6 +461,30 @@ interface IBosonAccountHandler is IBosonAccountEvents, BosonErrors { uint256 _sellerId ) external view returns (address defaultVoucherAddress, BosonTypes.Collection[] memory additionalCollections); + /** + * @notice Gets the details about all seller's collections. + * Use getSellersCollectionCount to get the total number of collections. + * + * @param _sellerId - the id of the seller to check + * @param _limit - the maximum number of Collections that should be returned starting from the index defined by `_offset`. If `_offset` + `_limit` exceeds total number of collections, `_limit` is adjusted to return all remaining collections. + * @param _offset - the starting index from which to return collections. If `_offset` is greater than or equal to total number of collections, an empty list is returned. + * @return defaultVoucherAddress - the address of the default voucher contract for the seller + * @return additionalCollections - an array of additional collections that the seller has created + */ + function getSellersCollectionsPaginated( + uint256 _sellerId, + uint256 _limit, + uint256 _offset + ) external view returns (address defaultVoucherAddress, BosonTypes.Collection[] memory additionalCollections); + + /** + * @notice Returns the number of additional collections for a seller. + * Use this in conjunction with getSellersCollectionsPaginated to get all collections. + * + * @param _sellerId - the id of the seller to check + */ + function getSellersCollectionCount(uint256 _sellerId) external view returns (uint256 collectionCount); + /** * @notice Returns the availability of salt for a seller. * diff --git a/contracts/protocol/facets/FundsHandlerFacet.sol b/contracts/protocol/facets/FundsHandlerFacet.sol index 06fc9199e..9d23fb973 100644 --- a/contracts/protocol/facets/FundsHandlerFacet.sol +++ b/contracts/protocol/facets/FundsHandlerFacet.sol @@ -179,16 +179,17 @@ contract FundsHandlerFacet is IBosonFundsHandler, ProtocolBase { uint256 _offset ) external view override returns (address[] memory tokenList) { address[] storage tokens = protocolLookups().tokenList[_entityId]; + uint256 tokenCount = tokens.length; - if (_offset >= tokens.length) { + if (_offset >= tokenCount) { return new address[](0); - } else if (_offset + _limit > tokens.length) { - _limit = tokens.length - _offset; + } else if (_offset + _limit > tokenCount) { + _limit = tokenCount - _offset; } tokenList = new address[](_limit); - for (uint i = 0; i < _limit; ) { + for (uint256 i = 0; i < _limit; ) { tokenList[i] = tokens[_offset++]; unchecked { diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index 90030d374..199593e65 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -717,7 +717,8 @@ contract SellerHandlerFacet is SellerBase { } /** - * @notice Gets the details about a seller's collections. + * @notice Gets the details about all seller's collections. + * In case seller has too many collections and this runs out of gas, please use getSellersCollectionsPaginated. * * @param _sellerId - the id of the seller to check * @return defaultVoucherAddress - the address of the default voucher contract for the seller @@ -730,6 +731,53 @@ contract SellerHandlerFacet is SellerBase { return (pl.cloneAddress[_sellerId], pl.additionalCollections[_sellerId]); } + /** + * @notice Gets the details about all seller's collections. + * Use getSellersCollectionCount to get the total number of collections. + * + * @param _sellerId - the id of the seller to check + * @param _limit - the maximum number of Collections that should be returned starting from the index defined by `_offset`. If `_offset` + `_limit` exceeds total number of collections, `_limit` is adjusted to return all remaining collections. + * @param _offset - the starting index from which to return collections. If `_offset` is greater than or equal to total number of collections, an empty list is returned. + * @return defaultVoucherAddress - the address of the default voucher contract for the seller + * @return additionalCollections - an array of additional collections that the seller has created + */ + function getSellersCollectionsPaginated( + uint256 _sellerId, + uint256 _limit, + uint256 _offset + ) external view returns (address defaultVoucherAddress, Collection[] memory additionalCollections) { + ProtocolLib.ProtocolLookups storage pl = protocolLookups(); + Collection[] storage sellersAdditionalCollections = pl.additionalCollections[_sellerId]; + uint256 collectionCount = sellersAdditionalCollections.length; + + if (_offset >= collectionCount) { + return (pl.cloneAddress[_sellerId], new Collection[](0)); + } else if (_offset + _limit > collectionCount) { + _limit = collectionCount - _offset; + } + + additionalCollections = new Collection[](_limit); + + for (uint256 i = 0; i < _limit; ) { + additionalCollections[i] = sellersAdditionalCollections[_offset++]; + unchecked { + i++; + } + } + + return (pl.cloneAddress[_sellerId], additionalCollections); + } + + /** + * @notice Returns the number of additional collections for a seller. + * Use this in conjunction with getSellersCollectionsPaginated to get all collections. + * + * @param _sellerId - the id of the seller to check + */ + function getSellersCollectionCount(uint256 _sellerId) external view returns (uint256 collectionCount) { + return protocolLookups().additionalCollections[_sellerId].length; + } + /** * @notice Returns the availability of salt for a seller. * diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index 95481f147..23e3e2560 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -3386,6 +3386,124 @@ describe("SellerHandler", function () { }); }); + context("👉 getSellersCollectionsPaginated()", async function () { + let externalId, expectedDefaultAddress, expectedCollectionAddress, additionalCollections; + + beforeEach(async function () { + // Create a seller + await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); + + expectedDefaultAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + admin.address + ); // default + + additionalCollections = new CollectionList([]); + for (let i = 1; i <= 5; i++) { + externalId = `Brand${i}`; + voucherInitValues.collectionSalt = encodeBytes32String(externalId); + expectedCollectionAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + admin.address, + voucherInitValues.collectionSalt + ); + voucherInitValues.contractURI = `https://brand${i}.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + // Add to expected collections + additionalCollections.collections.push(new Collection(expectedCollectionAddress, externalId)); + } + }); + + it("should return correct collection list", async function () { + const limit = 3; + const offset = 1; + + const expectedCollections = new CollectionList(additionalCollections.collections.slice(offset, offset + limit)); + + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollectionsPaginated(seller.id, limit, offset); + const returnedCollections = CollectionList.fromStruct(collections); + + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(returnedCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + + it("Offset is more than number of collections", async function () { + const limit = 2; + const offset = 8; + + const expectedCollections = new CollectionList([]); // empty + + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollectionsPaginated(seller.id, limit, offset); + const returnedCollections = CollectionList.fromStruct(collections); + + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(returnedCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + + it("Limit + offset is more than number of collections", async function () { + const limit = 7; + const offset = 2; + + const expectedCollections = new CollectionList(additionalCollections.collections.slice(offset)); // everything after offset + + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollectionsPaginated(seller.id, limit, offset); + const returnedCollections = CollectionList.fromStruct(collections); + + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(returnedCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + }); + + context("👉 getSellersCollectionCount()", async function () { + beforeEach(async function () { + // Create a seller + await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); + }); + + it("seller has no additional collections", async function () { + const expectedCollectionCount = 0; + const sellersCollectionCount = await accountHandler.connect(rando).getSellersCollectionCount(seller.id); + + expect(sellersCollectionCount).to.equal(expectedCollectionCount, "Incorrect number of collections"); + }); + + it("seller has additional collections", async function () { + const expectedCollectionCount = 4; + + for (let i = 1; i <= expectedCollectionCount; i++) { + const externalId = `Brand${i}`; + voucherInitValues.collectionSalt = encodeBytes32String(externalId); + voucherInitValues.contractURI = `https://brand${i}.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + } + + const sellersCollectionCount = await accountHandler.connect(rando).getSellersCollectionCount(seller.id); + + expect(sellersCollectionCount).to.equal(expectedCollectionCount, "Incorrect number of collections"); + }); + + it("seller does not exist", async function () { + const sellerId = 200; + const expectedCollectionCount = 0; + const sellersCollectionCount = await accountHandler.connect(rando).getSellersCollectionCount(sellerId); + + expect(sellersCollectionCount).to.equal(expectedCollectionCount, "Incorrect number of collections"); + }); + }); + context("👉 updateSellerSalt()", async function () { let newSellerSalt;