diff --git a/contracts/domain/BosonConstants.sol b/contracts/domain/BosonConstants.sol index bb7563223..2541255f8 100644 --- a/contracts/domain/BosonConstants.sol +++ b/contracts/domain/BosonConstants.sol @@ -94,6 +94,7 @@ string constant INVALID_DISPUTE_RESOLVER = "Invalid dispute resolver"; string constant INVALID_QUANTITY_AVAILABLE = "Invalid quantity available"; string constant DR_UNSUPPORTED_FEE = "Dispute resolver does not accept this token"; string constant AGENT_FEE_AMOUNT_TOO_HIGH = "Sum of agent fee amount and protocol fee amount should be <= offer fee limit"; +string constant NO_SUCH_COLLECTION = "No such collection"; // Revert Reasons: Group related string constant NO_SUCH_GROUP = "No such group"; diff --git a/contracts/domain/BosonTypes.sol b/contracts/domain/BosonTypes.sol index b24fb26ee..27098ed4c 100644 --- a/contracts/domain/BosonTypes.sol +++ b/contracts/domain/BosonTypes.sol @@ -146,6 +146,7 @@ contract BosonTypes { string metadataUri; string metadataHash; bool voided; + uint256 collectionIndex; } struct OfferDates { @@ -285,4 +286,9 @@ contract BosonTypes { string contractURI; uint256 royaltyPercentage; } + + struct Collection { + address collectionAddress; + string externalId; + } } diff --git a/contracts/interfaces/IInitializableVoucherClone.sol b/contracts/interfaces/IInitializableVoucherClone.sol index c07137496..643390d71 100644 --- a/contracts/interfaces/IInitializableVoucherClone.sol +++ b/contracts/interfaces/IInitializableVoucherClone.sol @@ -15,11 +15,13 @@ interface IInitializableVoucherClone { * @notice Initializes a voucher with the given parameters. * * @param _sellerId - The ID of the seller. + * @param _collectionIndex - The index of the collection. * @param _newOwner - The address of the new owner. * @param _voucherInitValues - The voucher initialization values. */ function initializeVoucher( uint256 _sellerId, + uint256 _collectionIndex, address _newOwner, BosonTypes.VoucherInitValues calldata _voucherInitValues ) external; diff --git a/contracts/interfaces/events/IBosonAccountEvents.sol b/contracts/interfaces/events/IBosonAccountEvents.sol index 4f307f99a..d1be8b315 100644 --- a/contracts/interfaces/events/IBosonAccountEvents.sol +++ b/contracts/interfaces/events/IBosonAccountEvents.sol @@ -68,4 +68,11 @@ interface IBosonAccountEvents { address indexed executedBy ); event AgentCreated(uint256 indexed agentId, BosonTypes.Agent agent, address indexed executedBy); + event CollectionCreated( + uint256 indexed sellerId, + uint256 collectionIndex, + address collectionAddress, + string indexed externalId, + address indexed executedBy + ); } diff --git a/contracts/interfaces/handlers/IBosonAccountHandler.sol b/contracts/interfaces/handlers/IBosonAccountHandler.sol index 9adb7d019..05fd89c08 100644 --- a/contracts/interfaces/handlers/IBosonAccountHandler.sol +++ b/contracts/interfaces/handlers/IBosonAccountHandler.sol @@ -9,7 +9,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: 0x15335ed7 + * The ERC-165 identifier for this interface is: 0x868de65b */ interface IBosonAccountHandler is IBosonAccountEvents { /** @@ -309,6 +309,23 @@ interface IBosonAccountHandler is IBosonAccountEvents { */ function removeSellersFromAllowList(uint256 _disputeResolverId, uint256[] calldata _sellerAllowList) external; + /** + * @notice Creates a new seller collection. + * + * Emits a CollectionCreated event if successful. + * + * Reverts if: + * - The offers region of protocol is paused + * - Caller is not the seller assistant + * + * @param _externalId - external collection id + * @param _voucherInitValues - the fully populated BosonTypes.VoucherInitValues struct + */ + function createNewCollection( + string calldata _externalId, + BosonTypes.VoucherInitValues calldata _voucherInitValues + ) external; + /** * @notice Gets the details about a seller. * @@ -353,6 +370,17 @@ interface IBosonAccountHandler is IBosonAccountEvents { BosonTypes.AuthToken calldata _associatedAuthToken ) external view returns (bool exists, BosonTypes.Seller memory seller, BosonTypes.AuthToken memory authToken); + /** + * @notice Gets the details about a seller's collections. + * + * @param _sellerId - the id of the seller to check + * @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 getSellersCollections( + uint256 _sellerId + ) external view returns (address defaultVoucherAddress, BosonTypes.Collection[] memory additionalCollections); + /** * @notice Gets the details about a buyer. * diff --git a/contracts/interfaces/handlers/IBosonOfferHandler.sol b/contracts/interfaces/handlers/IBosonOfferHandler.sol index bdad13bdf..dfd5d2ad9 100644 --- a/contracts/interfaces/handlers/IBosonOfferHandler.sol +++ b/contracts/interfaces/handlers/IBosonOfferHandler.sol @@ -9,7 +9,7 @@ import { IBosonOfferEvents } from "../events/IBosonOfferEvents.sol"; * * @notice Handles creation, voiding, and querying of offers within the protocol. * - * The ERC-165 identifier for this interface is: 0xa1598d02 + * The ERC-165 identifier for this interface is: 0xa1e3b91c */ interface IBosonOfferHandler is IBosonOfferEvents { /** @@ -35,6 +35,7 @@ interface IBosonOfferHandler is IBosonOfferEvents { * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -79,6 +80,7 @@ interface IBosonOfferHandler is IBosonOfferEvents { * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent ids are non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit diff --git a/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol b/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol index e686a0d40..5ccce28eb 100644 --- a/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol +++ b/contracts/interfaces/handlers/IBosonOrchestrationHandler.sol @@ -13,7 +13,7 @@ import { IBosonBundleEvents } from "../events/IBosonBundleEvents.sol"; * * @notice Combines creation of multiple entities (accounts, offers, groups, twins, bundles) in a single transaction * - * The ERC-165 identifier for this interface is: 0x0c62d8e3 + * The ERC-165 identifier for this interface is: 0xb2539c77 */ interface IBosonOrchestrationHandler is IBosonAccountEvents, @@ -91,6 +91,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -161,6 +162,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -218,6 +220,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -269,6 +272,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -323,6 +327,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When adding to the group if: * - Group does not exists * - Caller is not the assistant of the group @@ -377,6 +382,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When adding to the group if: * - Group does not exists * - Caller is not the assistant of the group @@ -435,6 +441,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -494,6 +501,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -558,6 +566,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -622,6 +631,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -705,6 +715,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -781,6 +792,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -861,6 +873,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -945,6 +958,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -1033,6 +1047,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -1122,6 +1137,7 @@ interface IBosonOrchestrationHandler is * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token diff --git a/contracts/protocol/bases/OfferBase.sol b/contracts/protocol/bases/OfferBase.sol index bb943e0bc..b327c709c 100644 --- a/contracts/protocol/bases/OfferBase.sol +++ b/contracts/protocol/bases/OfferBase.sol @@ -150,46 +150,59 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { // quantity must be greater than zero require(_offer.quantityAvailable > 0, INVALID_QUANTITY_AVAILABLE); - // Specified resolver must be registered and active, except for absolute zero offers with unspecified dispute resolver. - // If price and sellerDeposit are 0, seller is not obliged to choose dispute resolver, which is done by setting _disputeResolverId to 0. - // In this case, there is no need to check the validity of the dispute resolver. However, if one (or more) of {price, sellerDeposit, _disputeResolverId} - // is different from 0, it must be checked that dispute resolver exists, supports the exchange token and seller is allowed to choose them. DisputeResolutionTerms memory disputeResolutionTerms; - if (_offer.price != 0 || _offer.sellerDeposit != 0 || _disputeResolverId != 0) { - ( - bool exists, - DisputeResolver storage disputeResolver, - DisputeResolverFee[] storage disputeResolverFees - ) = fetchDisputeResolver(_disputeResolverId); - require(exists && disputeResolver.active, INVALID_DISPUTE_RESOLVER); - - // Operate in a block to avoid "stack too deep" error - { - // Cache protocol lookups for reference - ProtocolLib.ProtocolLookups storage lookups = protocolLookups(); - - // check that seller is on the DR allow list - if (lookups.allowedSellers[_disputeResolverId].length > 0) { - // if length == 0, dispute resolver allows any seller - // if length > 0, we check that it is on allow list - require(lookups.allowedSellerIndex[_disputeResolverId][_offer.sellerId] > 0, SELLER_NOT_APPROVED); + { + // Cache protocol lookups for reference + ProtocolLib.ProtocolLookups storage lookups = protocolLookups(); + + // Specified resolver must be registered and active, except for absolute zero offers with unspecified dispute resolver. + // If price and sellerDeposit are 0, seller is not obliged to choose dispute resolver, which is done by setting _disputeResolverId to 0. + // In this case, there is no need to check the validity of the dispute resolver. However, if one (or more) of {price, sellerDeposit, _disputeResolverId} + // is different from 0, it must be checked that dispute resolver exists, supports the exchange token and seller is allowed to choose them. + if (_offer.price != 0 || _offer.sellerDeposit != 0 || _disputeResolverId != 0) { + ( + bool exists, + DisputeResolver storage disputeResolver, + DisputeResolverFee[] storage disputeResolverFees + ) = fetchDisputeResolver(_disputeResolverId); + require(exists && disputeResolver.active, INVALID_DISPUTE_RESOLVER); + + // Operate in a block to avoid "stack too deep" error + { + // check that seller is on the DR allow list + if (lookups.allowedSellers[_disputeResolverId].length > 0) { + // if length == 0, dispute resolver allows any seller + // if length > 0, we check that it is on allow list + require( + lookups.allowedSellerIndex[_disputeResolverId][_offer.sellerId] > 0, + SELLER_NOT_APPROVED + ); + } + + // get the index of DisputeResolverFee and make sure DR supports the exchangeToken + uint256 feeIndex = lookups.disputeResolverFeeTokenIndex[_disputeResolverId][_offer.exchangeToken]; + require(feeIndex > 0, DR_UNSUPPORTED_FEE); + + uint256 feeAmount = disputeResolverFees[feeIndex - 1].feeAmount; + + // store DR terms + disputeResolutionTerms.disputeResolverId = _disputeResolverId; + disputeResolutionTerms.escalationResponsePeriod = disputeResolver.escalationResponsePeriod; + disputeResolutionTerms.feeAmount = feeAmount; + disputeResolutionTerms.buyerEscalationDeposit = + (feeAmount * protocolFees().buyerEscalationDepositPercentage) / + 10000; + + protocolEntities().disputeResolutionTerms[_offer.id] = disputeResolutionTerms; } + } - // get the index of DisputeResolverFee and make sure DR supports the exchangeToken - uint256 feeIndex = lookups.disputeResolverFeeTokenIndex[_disputeResolverId][_offer.exchangeToken]; - require(feeIndex > 0, DR_UNSUPPORTED_FEE); - - uint256 feeAmount = disputeResolverFees[feeIndex - 1].feeAmount; - - // store DR terms - disputeResolutionTerms.disputeResolverId = _disputeResolverId; - disputeResolutionTerms.escalationResponsePeriod = disputeResolver.escalationResponsePeriod; - disputeResolutionTerms.feeAmount = feeAmount; - disputeResolutionTerms.buyerEscalationDeposit = - (feeAmount * protocolFees().buyerEscalationDepositPercentage) / - 10000; - - protocolEntities().disputeResolutionTerms[_offer.id] = disputeResolutionTerms; + // Collection must exist. Collections with index 0 exist by default. + if (_offer.collectionIndex > 0) { + require( + lookups.additionalCollections[_offer.sellerId].length >= _offer.collectionIndex, + NO_SUCH_COLLECTION + ); } } @@ -244,6 +257,7 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { offer.exchangeToken = _offer.exchangeToken; offer.metadataUri = _offer.metadataUri; offer.metadataHash = _offer.metadataHash; + offer.collectionIndex = _offer.collectionIndex; // Get storage location for offer dates OfferDates storage offerDates = fetchOfferDates(_offer.id); @@ -316,7 +330,9 @@ contract OfferBase is ProtocolBase, IBosonOfferEvents { ProtocolLib.ProtocolCounters storage pc = protocolCounters(); uint256 _startId = pc.nextExchangeId; - IBosonVoucher bosonVoucher = IBosonVoucher(protocolLookups().cloneAddress[offer.sellerId]); + IBosonVoucher bosonVoucher = IBosonVoucher( + getCloneAddress(protocolLookups(), offer.sellerId, offer.collectionIndex) + ); address sender = msgSender(); diff --git a/contracts/protocol/bases/ProtocolBase.sol b/contracts/protocol/bases/ProtocolBase.sol index 1690d1a3a..5f80337f0 100644 --- a/contracts/protocol/bases/ProtocolBase.sol +++ b/contracts/protocol/bases/ProtocolBase.sol @@ -669,4 +669,25 @@ abstract contract ProtocolBase is PausableBase, ReentrancyGuardBase { // Determine existence exists = (_exchangeId > 0 && condition.method != EvaluationMethod.None); } + + /** + * @notice Fetches a clone address from storage by seller id and collection index + * If the collection index is 0, the clone address is the seller's main collection, + * otherwise it is the clone address of the additional collection at the given index. + * + * @param _lookups - storage slot for protocol lookups + * @param _sellerId - the id of the seller + * @param _collectionIndex - the index of the collection + * @return cloneAddress - the clone address + */ + function getCloneAddress( + ProtocolLib.ProtocolLookups storage _lookups, + uint256 _sellerId, + uint256 _collectionIndex + ) internal view returns (address cloneAddress) { + return + _collectionIndex == 0 + ? _lookups.cloneAddress[_sellerId] + : _lookups.additionalCollections[_sellerId][_collectionIndex - 1].collectionAddress; + } } diff --git a/contracts/protocol/bases/SellerBase.sol b/contracts/protocol/bases/SellerBase.sol index b182e7452..6f5488dc2 100644 --- a/contracts/protocol/bases/SellerBase.sol +++ b/contracts/protocol/bases/SellerBase.sol @@ -96,7 +96,7 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { storeSeller(_seller, _authToken, lookups); // Create clone and store its address cloneAddress - address voucherCloneAddress = cloneBosonVoucher(sellerId, _seller.assistant, _voucherInitValues); + address voucherCloneAddress = cloneBosonVoucher(sellerId, 0, _seller.assistant, _voucherInitValues); lookups.cloneAddress[sellerId] = voucherCloneAddress; // Notify watchers of state change @@ -148,12 +148,14 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { * @notice Creates a minimal clone of the Boson Voucher Contract. * * @param _sellerId - id of the seller + * @param _collectionIndex - index of the collection. * @param _assistant - address of the assistant * @param _voucherInitValues - the fully populated BosonTypes.VoucherInitValues struct * @return cloneAddress - the address of newly created clone */ function cloneBosonVoucher( uint256 _sellerId, + uint256 _collectionIndex, address _assistant, VoucherInitValues calldata _voucherInitValues ) internal returns (address cloneAddress) { @@ -174,7 +176,12 @@ contract SellerBase is ProtocolBase, IBosonAccountEvents { // Initialize the clone IInitializableVoucherClone(cloneAddress).initialize(pa.voucherBeacon); - IInitializableVoucherClone(cloneAddress).initializeVoucher(_sellerId, _assistant, _voucherInitValues); + IInitializableVoucherClone(cloneAddress).initializeVoucher( + _sellerId, + _collectionIndex, + _assistant, + _voucherInitValues + ); } /** diff --git a/contracts/protocol/clients/voucher/BosonVoucher.sol b/contracts/protocol/clients/voucher/BosonVoucher.sol index 3cad102b2..a29fb627a 100644 --- a/contracts/protocol/clients/voucher/BosonVoucher.sol +++ b/contracts/protocol/clients/voucher/BosonVoucher.sol @@ -69,10 +69,11 @@ contract BosonVoucherBase is IBosonVoucher, BeaconClientBase, OwnableUpgradeable */ function initializeVoucher( uint256 _sellerId, + uint256 _collectionIndex, address _newOwner, VoucherInitValues calldata voucherInitValues ) public initializer { - string memory sellerId = Strings.toString(_sellerId); + string memory sellerId = string.concat(Strings.toString(_sellerId), "_", Strings.toString(_collectionIndex)); string memory voucherName = string.concat(VOUCHER_NAME, " ", sellerId); string memory voucherSymbol = string.concat(VOUCHER_SYMBOL, "_", sellerId); diff --git a/contracts/protocol/facets/ExchangeHandlerFacet.sol b/contracts/protocol/facets/ExchangeHandlerFacet.sol index 44855b29d..1d220130a 100644 --- a/contracts/protocol/facets/ExchangeHandlerFacet.sol +++ b/contracts/protocol/facets/ExchangeHandlerFacet.sol @@ -121,7 +121,7 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { (, Offer storage offer) = fetchOffer(_offerId); // Make sure that the voucher was issued on the clone that is making a call - require(msg.sender == protocolLookups().cloneAddress[offer.sellerId], ACCESS_DENIED); + require(msg.sender == getCloneAddress(protocolLookups(), offer.sellerId, offer.collectionIndex), ACCESS_DENIED); // Exchange must not exist already (bool exists, ) = fetchExchange(_exchangeId); @@ -225,7 +225,9 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { // Issue voucher, unless it already exist (for preminted offers) lookups.voucherCount[buyerId]++; if (!_isPreminted) { - IBosonVoucher bosonVoucher = IBosonVoucher(lookups.cloneAddress[_offer.sellerId]); + IBosonVoucher bosonVoucher = IBosonVoucher( + getCloneAddress(lookups, _offer.sellerId, _offer.collectionIndex) + ); uint256 tokenId = _exchangeId | (_offerId << 128); bosonVoucher.issueVoucher(tokenId, _buyer); } @@ -520,7 +522,7 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { (, Offer storage offer) = fetchOffer(exchange.offerId); // Make sure that the voucher was issued on the clone that is making a call - require(msg.sender == lookups.cloneAddress[offer.sellerId], ACCESS_DENIED); + require(msg.sender == getCloneAddress(lookups, offer.sellerId, offer.collectionIndex), ACCESS_DENIED); // Decrease voucher counter for old buyer lookups.voucherCount[exchange.buyerId]--; @@ -663,7 +665,7 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { // Burn the voucher uint256 offerId = _exchange.offerId; (, Offer storage offer) = fetchOffer(offerId); - IBosonVoucher bosonVoucher = IBosonVoucher(lookups.cloneAddress[offer.sellerId]); + IBosonVoucher bosonVoucher = IBosonVoucher(getCloneAddress(lookups, offer.sellerId, offer.collectionIndex)); uint256 tokenId = _exchange.id; if (tokenId >= EXCHANGE_ID_2_2_0) tokenId |= (offerId << 128); diff --git a/contracts/protocol/facets/OfferHandlerFacet.sol b/contracts/protocol/facets/OfferHandlerFacet.sol index 58a379e1a..6c5af99e1 100644 --- a/contracts/protocol/facets/OfferHandlerFacet.sol +++ b/contracts/protocol/facets/OfferHandlerFacet.sol @@ -43,6 +43,7 @@ contract OfferHandlerFacet is IBosonOfferHandler, OfferBase { * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -89,6 +90,7 @@ contract OfferHandlerFacet is IBosonOfferHandler, OfferBase { * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent ids are non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit diff --git a/contracts/protocol/facets/OrchestrationHandlerFacet1.sol b/contracts/protocol/facets/OrchestrationHandlerFacet1.sol index 5e51ee04e..aba46dbe2 100644 --- a/contracts/protocol/facets/OrchestrationHandlerFacet1.sol +++ b/contracts/protocol/facets/OrchestrationHandlerFacet1.sol @@ -68,6 +68,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -141,6 +142,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When agent id is non zero: * - If Agent does not exist * - If the sum of agent fee amount and protocol fee amount is greater than the offer fee limit @@ -210,6 +212,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -275,6 +278,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -331,6 +335,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When adding to the group if: * - Group does not exists * - Caller is not the assistant of the group @@ -393,6 +398,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When adding to the group if: * - Group does not exists * - Caller is not the assistant of the group @@ -454,6 +460,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -519,6 +526,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -588,6 +596,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -659,6 +668,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -753,6 +763,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -834,6 +845,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When agent id is non zero: * - If Agent does not exist @@ -927,6 +939,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -1016,6 +1029,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - When creating twin if * - Not approved to transfer the seller's token * - SupplyAvailable is zero @@ -1117,6 +1131,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token @@ -1219,6 +1234,7 @@ contract OrchestrationHandlerFacet1 is PausableBase, SellerBase, OfferBase, Grou * - Seller is not on dispute resolver's seller allow list * - Dispute resolver does not accept fees in the exchange token * - Buyer cancel penalty is greater than price + * - Collection does not exist * - Condition includes invalid combination of parameters * - When creating twin if * - Not approved to transfer the seller's token diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index cc03b32ec..778481673 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -271,7 +271,13 @@ contract SellerHandlerFacet is SellerBase { seller.assistant = sender; // Transfer ownership of voucher contract to new assistant - IBosonVoucher(lookups.cloneAddress[_sellerId]).transferOwnership(sender); + IBosonVoucher(lookups.cloneAddress[_sellerId]).transferOwnership(sender); // default voucher contract + Collection[] storage sellersAdditionalCollections = lookups.additionalCollections[_sellerId]; + uint256 collectionCount = sellersAdditionalCollections.length; + for (i = 0; i < collectionCount; i++) { + // Additional collections (if they exist) + IBosonVoucher(sellersAdditionalCollections[i].collectionAddress).transferOwnership(sender); + } // Store new seller id by assistant mapping lookups.sellerIdByAssistant[sender] = _sellerId; @@ -331,6 +337,41 @@ contract SellerHandlerFacet is SellerBase { } } + /** + * @notice Creates a new seller collection. + * + * Emits a CollectionCreated event if successful. + * + * Reverts if: + * - The offers region of protocol is paused + * - Caller is not the seller assistant + * + * @param _externalId - external collection id + * @param _voucherInitValues - the fully populated BosonTypes.VoucherInitValues struct + */ + function createNewCollection( + string calldata _externalId, + VoucherInitValues calldata _voucherInitValues + ) external sellersNotPaused { + address assistant = msgSender(); + + (bool exists, uint256 sellerId) = getSellerIdByAssistant(assistant); + require(exists, NO_SUCH_SELLER); + + Collection[] storage sellersAdditionalCollections = protocolLookups().additionalCollections[sellerId]; + uint256 collectionIndex = sellersAdditionalCollections.length + 1; // 0 is reserved for the original collection + + // Create clone and store its address to additionalCollections + address voucherCloneAddress = cloneBosonVoucher(sellerId, collectionIndex, assistant, _voucherInitValues); + + // Store collection details + Collection storage newCollection = sellersAdditionalCollections.push(); + newCollection.collectionAddress = voucherCloneAddress; + newCollection.externalId = _externalId; + + emit CollectionCreated(sellerId, collectionIndex, voucherCloneAddress, _externalId, assistant); + } + /** * @notice Gets the details about a seller. * @@ -396,6 +437,20 @@ contract SellerHandlerFacet is SellerBase { } } + /** + * @notice Gets the details about a seller's collections. + * + * @param _sellerId - the id of the seller to check + * @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 getSellersCollections( + uint256 _sellerId + ) external view returns (address defaultVoucherAddress, Collection[] memory additionalCollections) { + ProtocolLib.ProtocolLookups storage pl = protocolLookups(); + return (pl.cloneAddress[_sellerId], pl.additionalCollections[_sellerId]); + } + /** * @notice Pre update Seller checks * diff --git a/contracts/protocol/libs/ProtocolLib.sol b/contracts/protocol/libs/ProtocolLib.sol index 044b13890..68d55f5a1 100644 --- a/contracts/protocol/libs/ProtocolLib.sol +++ b/contracts/protocol/libs/ProtocolLib.sol @@ -187,6 +187,8 @@ library ProtocolLib { mapping(uint256 => BosonTypes.AuthToken) pendingAuthTokenUpdatesBySeller; // dispute resolver id => DisputeResolver mapping(uint256 => BosonTypes.DisputeResolver) pendingAddressUpdatesByDisputeResolver; + // seller id => list of additional collections (address + external id) + mapping(uint256 => BosonTypes.Collection[]) additionalCollections; } // Incrementing id counters diff --git a/scripts/config/revert-reasons.js b/scripts/config/revert-reasons.js index a87bd58c6..767aa5118 100644 --- a/scripts/config/revert-reasons.js +++ b/scripts/config/revert-reasons.js @@ -47,6 +47,7 @@ exports.RevertReasons = { OFFER_NOT_AVAILABLE: "Offer is not yet available", OFFER_HAS_EXPIRED: "Offer has expired", OFFER_SOLD_OUT: "Offer has sold out", + NO_SUCH_COLLECTION: "No such collection", // Group related NO_SUCH_GROUP: "No such group", diff --git a/scripts/domain/Collection.js b/scripts/domain/Collection.js new file mode 100644 index 000000000..c50b17e8a --- /dev/null +++ b/scripts/domain/Collection.js @@ -0,0 +1,200 @@ +const { stringIsValid, addressIsValid } = require("../util/validations.js"); + +/** + * Boson Protocol Domain Entity: Collection + * + * See: {BosonTypes.Collection} + */ +class Collection { + /* + struct Collection { + address collectionAddress; + string externalId; + } + */ + + constructor(collectionAddress, externalId) { + this.collectionAddress = collectionAddress; + this.externalId = externalId; + } + + /** + * Get a new Collection instance from a pojo representation + * @param o + * @returns {Collection} + */ + static fromObject(o) { + const { collectionAddress, externalId } = o; + return new Collection(collectionAddress, externalId); + } + + /** + * Get a new Collection instance from a returned struct representation + * @param struct + * @returns {*} + */ + static fromStruct(struct) { + let collectionAddress, externalId; + + // destructure struct + [collectionAddress, externalId] = struct; + + return Collection.fromObject({ + collectionAddress, + externalId, + }); + } + + /** + * Get a database representation of this Collection instance + * @returns {object} + */ + toObject() { + return JSON.parse(this.toString()); + } + + /** + * Get a string representation of this Collection instance + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * Get a struct representation of this Collection instance + * @returns {string} + */ + toStruct() { + return [this.collectionAddress, this.externalId]; + } + + /** + * Clone this Collection + * @returns {Collection} + */ + clone() { + return Collection.fromObject(this.toObject()); + } + + /** + * Is this Collection instance's collectionAddress field valid? + * Must be a eip55 compliant Ethereum address + * @returns {boolean} + */ + collectionAddressIsValid() { + return addressIsValid(this.collectionAddress); + } + + /** + * Is this Collection instance's externalId field valid? + * Always present, must be a string + * @returns {boolean} + */ + externalIdIsValid() { + return stringIsValid(this.externalId); + } + + /** + * Is this Collection instance valid? + * @returns {boolean} + */ + isValid() { + return this.collectionAddressIsValid() && this.externalIdIsValid(); + } +} + +/** + * Boson Protocol Domain Entity: Collection of Collection + * + * See: {BosonTypes.Collection} + */ +class CollectionList { + constructor(collections) { + this.collections = collections; + } + + /** + * Get a new CollectionList instance from a pojo representation + * @param o + * @returns {CollectionList} + */ + static fromObject(o) { + const { collections } = o; + return new CollectionList(collections.map((d) => Collection.fromObject(d))); + } + + /** + * Get a new CollectionList instance from a returned struct representation + * @param struct + * @returns {*} + */ + static fromStruct(struct) { + return CollectionList.fromObject({ + collections: struct.map((collections) => Collection.fromStruct(collections)), + }); + } + + /** + * Get a database representation of this CollectionList instance + * @returns {object} + */ + toObject() { + return JSON.parse(this.toString()); + } + + /** + * Get a string representation of this CollectionList instance + * @returns {string} + */ + toString() { + return JSON.stringify(this); + } + + /** + * Get a struct representation of this CollectionList instance + * @returns {string} + */ + toStruct() { + return this.collections.map((d) => d.toStruct()); + } + + /** + * Clone this CollectionList + * @returns {CollectionList} + */ + clone() { + return CollectionList.fromObject(this.toObject()); + } + + /** + * Is this CollectionList instance's collection field valid? + * Must be a list of Collection instances + * @returns {boolean} + */ + collectionIsValid() { + let valid = false; + let { collections } = this; + try { + valid = + Array.isArray(collections) && + collections.reduce( + (previousCollections, currentCollections) => previousCollections && currentCollections.isValid(), + true + ); + } catch (e) {} + return valid; + } + + /** + * Is this CollectionList instance valid? + * @returns {boolean} + */ + isValid() { + return this.collectionIsValid(); + } +} + +// Export +exports.Collection = Collection; +exports.CollectionList = CollectionList; diff --git a/scripts/domain/Offer.js b/scripts/domain/Offer.js index fdaddbc5a..1e020a565 100644 --- a/scripts/domain/Offer.js +++ b/scripts/domain/Offer.js @@ -18,6 +18,7 @@ class Offer { string metadataUri; string metadataHash; bool voided; + uint256 collectionIndex; } */ @@ -31,7 +32,8 @@ class Offer { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ) { this.id = id; this.sellerId = sellerId; @@ -43,6 +45,7 @@ class Offer { this.metadataUri = metadataUri; this.metadataHash = metadataHash; this.voided = voided; + this.collectionIndex = collectionIndex; } /** @@ -62,6 +65,7 @@ class Offer { metadataUri, metadataHash, voided, + collectionIndex, } = o; return new Offer( @@ -74,7 +78,8 @@ class Offer { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); } @@ -93,7 +98,8 @@ class Offer { exchangeToken, metadataUri, metadataHash, - voided; + voided, + collectionIndex; // destructure struct [ @@ -107,6 +113,7 @@ class Offer { metadataUri, metadataHash, voided, + collectionIndex, ] = struct; return Offer.fromObject({ @@ -120,6 +127,7 @@ class Offer { metadataUri, metadataHash, voided, + collectionIndex: collectionIndex.toString(), }); } @@ -155,6 +163,7 @@ class Offer { this.metadataUri, this.metadataHash, this.voided, + this.collectionIndex, ]; } @@ -259,6 +268,15 @@ class Offer { return booleanIsValid(this.voided); } + /** + * Is this Offer instance's collectionIndex field valid? + * Must be a string representation of a big number + * @returns {boolean} + */ + collectionIndexIsValid() { + return bigNumberIsValid(this.collectionIndex); + } + /** * Is this Offer instance valid? * @returns {boolean} @@ -274,7 +292,8 @@ class Offer { this.exchangeTokenIsValid() && this.metadataUriIsValid() && this.metadataHashIsValid() && - this.voidedIsValid() + this.voidedIsValid() && + this.collectionIndexIsValid() ); } } diff --git a/test/domain/CollectionTest.js b/test/domain/CollectionTest.js new file mode 100644 index 000000000..35963cf1e --- /dev/null +++ b/test/domain/CollectionTest.js @@ -0,0 +1,324 @@ +const hre = require("hardhat"); +const ethers = hre.ethers; +const { expect } = require("chai"); +const { Collection, CollectionList } = require("../../scripts/domain/Collection"); + +/** + * Test the Collection domain entity + */ +describe("Collection", function () { + // Suite-wide scope + let accounts, collection, object, promoted, clone, dehydrated, rehydrated, key, value, struct; + let collectionAddress, externalId; + + beforeEach(async function () { + // Get a list of accounts + accounts = await ethers.getSigners(); + collectionAddress = accounts[1].address; + + // Required constructor params + externalId = "Brand1"; + }); + + context("📋 Constructor", async function () { + it("Should allow creation of valid, fully populated Collection instance", async function () { + // Create valid collection + collection = new Collection(collectionAddress, externalId); + expect(collection.collectionAddressIsValid()).is.true; + expect(collection.externalIdIsValid()).is.true; + expect(collection.isValid()).is.true; + }); + }); + + context("📋 Field validations", async function () { + beforeEach(async function () { + // Create valid collection, then set fields in tests directly + collection = new Collection(collectionAddress, externalId); + expect(collection.isValid()).is.true; + }); + + it("Always present, collectionAddress must be a string representation of an EIP-55 compliant address", async function () { + // Invalid field value + collection.collectionAddress = "0xASFADF"; + expect(collection.collectionAddressIsValid()).is.false; + expect(collection.isValid()).is.false; + + // Invalid field value + collection.collectionAddress = "zedzdeadbaby"; + expect(collection.collectionAddressIsValid()).is.false; + expect(collection.isValid()).is.false; + + // Valid field value + collection.collectionAddress = accounts[0].address; + expect(collection.collectionAddressIsValid()).is.true; + expect(collection.isValid()).is.true; + + // Valid field value + collection.collectionAddress = "0xec2fd5bd6fc7b576dae82c0b9640969d8de501a2"; + expect(collection.collectionAddressIsValid()).is.true; + expect(collection.isValid()).is.true; + }); + + it("Always present, externalId must be a string", async function () { + // Invalid field value + collection.externalId = 12; + expect(collection.externalIdIsValid()).is.false; + expect(collection.isValid()).is.false; + + // Valid field value + collection.externalId = "zedzdeadbaby"; + expect(collection.externalIdIsValid()).is.true; + expect(collection.isValid()).is.true; + + // Valid field value + collection.externalId = "QmYXc12ov6F2MZVZwPs5XeCBbf61cW3wKRk8h3D5NTYj4T"; + expect(collection.externalIdIsValid()).is.true; + expect(collection.isValid()).is.true; + + // Valid field value + collection.externalId = ""; + expect(collection.externalIdIsValid()).is.true; + expect(collection.isValid()).is.true; + }); + }); + + context("📋 Utility functions", async function () { + beforeEach(async function () { + // Create valid collection, then set fields in tests directly + collection = new Collection(collectionAddress, externalId); + + expect(collection.isValid()).is.true; + + // Get plain object + object = { + collectionAddress, + externalId, + }; + + // Struct representation + struct = [collectionAddress, externalId]; + }); + + context("👉 Static", async function () { + it("Collection.fromObject() should return a Collection instance with the same values as the given plain object", async function () { + // Promote to instance + promoted = Collection.fromObject(object); + + // Is a Collection instance + expect(promoted instanceof Collection).is.true; + + // Key values all match + for ([key, value] of Object.entries(collection)) { + expect(JSON.stringify(promoted[key]) === JSON.stringify(value)).is.true; + } + }); + + it("Collection.fromStruct() should return a Collection instance from a struct representation", async function () { + // Get an instance from the struct + collection = Collection.fromStruct(struct); + + // Ensure it is valid + expect(collection.isValid()).to.be.true; + }); + }); + + context("👉 Instance", async function () { + it("instance.toString() should return a JSON string representation of the Collection instance", async function () { + dehydrated = collection.toString(); + rehydrated = JSON.parse(dehydrated); + + for ([key, value] of Object.entries(collection)) { + expect(JSON.stringify(rehydrated[key]) === JSON.stringify(value)).is.true; + } + }); + + it("instance.toObject() should return a plain object representation of the Collection instance", async function () { + // Get plain object + object = collection.toObject(); + + // Not a Collection instance + expect(object instanceof Collection).is.false; + + // Key values all match + for ([key, value] of Object.entries(collection)) { + expect(JSON.stringify(object[key]) === JSON.stringify(value)).is.true; + } + }); + + it("Collection.toStruct() should return a struct representation of the Collection instance", async function () { + // Get struct from collection + struct = collection.toStruct(); + + // Marshal back to a collection instance + collection = Collection.fromStruct(struct); + + // Ensure it marshals back to a valid collection + expect(collection.isValid()).to.be.true; + }); + + it("instance.clone() should return another Collection instance with the same property values", async function () { + // Get plain object + clone = collection.clone(); + + // Is a Collection instance + expect(clone instanceof Collection).is.true; + + // Key values all match + for ([key, value] of Object.entries(collection)) { + expect(JSON.stringify(clone[key]) === JSON.stringify(value)).is.true; + } + }); + }); + }); +}); + +describe("CollectionList", function () { + // Suite-wide scope + let accounts, collections, collectionList, object, promoted, clone, dehydrated, rehydrated, key, value, struct; + + beforeEach(async function () { + // Get a list of accounts + accounts = await ethers.getSigners(); + + // Required constructor params + collections = [ + new Collection(accounts[1].address, "MockToken1", "100"), + new Collection(accounts[2].address, "MockToken2", "200"), + new Collection(accounts[3].address, "MockToken3", "300"), + ]; + }); + + context("📋 Constructor", async function () { + it("Should allow creation of valid, fully populated CollectionList instance", async function () { + // Create valid CollectionList + collectionList = new CollectionList(collections); + expect(collectionList.collectionIsValid()).is.true; + expect(collectionList.isValid()).is.true; + }); + }); + + context("📋 Field validations", async function () { + beforeEach(async function () { + // Create valid CollectionList, then set fields in tests directly + collectionList = new CollectionList(collections); + expect(collectionList.isValid()).is.true; + }); + + it("Always present, collections must be an array of valid Collection instances", async function () { + // Invalid field value + collectionList.collections = "0xASFADF"; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collection = collections[0]; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collections = ["0xASFADF", "zedzdeadbaby"]; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collections = undefined; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collections = [...collections, "zedzdeadbaby"]; + expect(collectionList.isValid()).is.false; + + // Invalid field value + collectionList.collections = [new Collection("111", "mockToken", "100")]; + expect(collectionList.isValid()).is.false; + + // Valid field value + collectionList.collections = [...collections]; + expect(collectionList.isValid()).is.true; + }); + }); + + context("📋 Utility functions", async function () { + beforeEach(async function () { + // Create valid CollectionList, then set fields in tests directly + collectionList = new CollectionList(collections); + expect(collectionList.isValid()).is.true; + + // Get plain object + object = { + collections, + }; + + // Struct representation + struct = collections.map((d) => d.toStruct()); + }); + + context("👉 Static", async function () { + it("CollectionList.fromObject() should return a CollectionList instance with the same values as the given plain object", async function () { + // Promote to instance + promoted = CollectionList.fromObject(object); + + // Is a CollectionList instance + expect(promoted instanceof CollectionList).is.true; + + // Key values all match + for ([key, value] of Object.entries(collectionList)) { + expect(JSON.stringify(promoted[key]) === JSON.stringify(value)).is.true; + } + }); + + it("CollectionList.fromStruct() should return a CollectionList instance from a struct representation", async function () { + // Get an instance from the struct + collectionList = CollectionList.fromStruct(struct); + + // Ensure it is valid + expect(collectionList.isValid()).to.be.true; + }); + }); + + context("👉 Instance", async function () { + it("instance.toString() should return a JSON string representation of the CollectionList instance", async function () { + dehydrated = collectionList.toString(); + rehydrated = JSON.parse(dehydrated); + + for ([key, value] of Object.entries(collectionList)) { + expect(JSON.stringify(rehydrated[key]) === JSON.stringify(value)).is.true; + } + }); + + it("instance.toObject() should return a plain object representation of the CollectionList instance", async function () { + // Get plain object + object = collectionList.toObject(); + + // Not a CollectionList instance + expect(object instanceof CollectionList).is.false; + + // Key values all match + for ([key, value] of Object.entries(collectionList)) { + expect(JSON.stringify(object[key]) === JSON.stringify(value)).is.true; + } + }); + + it("CollectionList.toStruct() should return a struct representation of the CollectionList instance", async function () { + // Get struct from CollectionList + struct = collectionList.toStruct(); + + // Marshal back to a CollectionList instance + collectionList = CollectionList.fromStruct(struct); + + // Ensure it marshals back to a valid CollectionList + expect(collectionList.isValid()).to.be.true; + }); + + it("instance.clone() should return another CollectionList instance with the same property values", async function () { + // Get plain object + clone = collectionList.clone(); + + // Is a CollectionList instance + expect(clone instanceof CollectionList).is.true; + + // Key values all match + for ([key, value] of Object.entries(collectionList)) { + expect(JSON.stringify(clone[key]) === JSON.stringify(value)).is.true; + } + }); + }); + }); +}); diff --git a/test/domain/OfferTest.js b/test/domain/OfferTest.js index 8ecdc7e8a..8fc255e1f 100644 --- a/test/domain/OfferTest.js +++ b/test/domain/OfferTest.js @@ -19,7 +19,8 @@ describe("Offer", function () { exchangeToken, metadataUri, metadataHash, - voided; + voided, + collectionIndex; beforeEach(async function () { // Get a list of accounts @@ -35,6 +36,7 @@ describe("Offer", function () { metadataHash = "QmYXc12ov6F2MZVZwPs5XeCBbf61cW3wKRk8h3D5NTYj4T"; // not an actual metadataHash, just some data for tests metadataUri = `https://ipfs.io/ipfs/${metadataHash}`; voided = false; + collectionIndex = "2"; }); context("📋 Constructor", async function () { @@ -50,7 +52,8 @@ describe("Offer", function () { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); expect(offer.idIsValid()).is.true; expect(offer.sellerIdIsValid()).is.true; @@ -63,6 +66,7 @@ describe("Offer", function () { expect(offer.metadataHashIsValid()).is.true; expect(offer.voidedIsValid()).is.true; expect(offer.isValid()).is.true; + expect(offer.collectionIndexIsValid()).is.true; }); }); @@ -79,7 +83,8 @@ describe("Offer", function () { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); expect(offer.isValid()).is.true; }); @@ -293,6 +298,33 @@ describe("Offer", function () { expect(offer.voidedIsValid()).is.true; expect(offer.isValid()).is.true; }); + + it("Always present, collectionIndex must be the string representation of a BigNumber", async function () { + // Invalid field value + offer.collectionIndex = "zedzdeadbaby"; + expect(offer.collectionIndexIsValid()).is.false; + expect(offer.isValid()).is.false; + + // Invalid field value + offer.collectionIndex = new Date(); + expect(offer.collectionIndexIsValid()).is.false; + expect(offer.isValid()).is.false; + + // Invalid field value + offer.collectionIndex = 12; + expect(offer.collectionIndexIsValid()).is.false; + expect(offer.isValid()).is.false; + + // Valid field value + offer.collectionIndex = "0"; + expect(offer.collectionIndexIsValid()).is.true; + expect(offer.isValid()).is.true; + + // Valid field value + offer.collectionIndex = "126"; + expect(offer.collectionIndexIsValid()).is.true; + expect(offer.isValid()).is.true; + }); }); context("📋 Utility functions", async function () { @@ -311,7 +343,8 @@ describe("Offer", function () { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); expect(offer.isValid()).is.true; @@ -327,6 +360,7 @@ describe("Offer", function () { metadataUri, metadataHash, voided, + collectionIndex, }; }); @@ -356,6 +390,7 @@ describe("Offer", function () { offer.metadataUri, offer.metadataHash, offer.voided, + offer.collectionIndex, ]; // Get struct diff --git a/test/protocol/ExchangeHandlerTest.js b/test/protocol/ExchangeHandlerTest.js index 203a952d0..67ea52c24 100644 --- a/test/protocol/ExchangeHandlerTest.js +++ b/test/protocol/ExchangeHandlerTest.js @@ -679,6 +679,45 @@ describe("IBosonExchangeHandler", function () { ); }); + it("should work on an additional collection", async function () { + // Create a new collection + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + exchangeId = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offer.id, exchangeId); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // expected address of the first clone and first additional collection + const defaultCloneAddress = calculateContractAddress(await accountHandler.getAddress(), "1"); + const defaultBosonVoucher = await getContractAt("BosonVoucher", defaultCloneAddress); + const additionalCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), "2"); + const additionalCollection = await getContractAt("BosonVoucher", additionalCollectionAddress); + + // buyer should own 1 voucher additional collection and 0 vouchers on the default clone + expect(await defaultBosonVoucher.balanceOf(buyer.address)).to.equal( + "0", + "Default clone: buyer's balance should be 0" + ); + expect(await additionalCollection.balanceOf(buyer.address)).to.equal( + "1", + "Additional collection: buyer's balance should be 1" + ); + + // Make sure that vouchers belong to correct buyers and that exist on the correct clone + await expect(defaultBosonVoucher.ownerOf(tokenId)).to.revertedWith(RevertReasons.ERC721_INVALID_TOKEN_ID); + expect(await additionalCollection.ownerOf(tokenId)).to.equal(buyer.address, "Wrong buyer address"); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -879,7 +918,7 @@ describe("IBosonExchangeHandler", function () { }); it("Should not decrement quantityAvailable", async function () { - // Offer qunantityAvailable should be decremented + // Offer quantityAvailable should be decremented let [, offer] = await offerHandler.connect(rando).getOffer(offerId); const quantityAvailableBefore = offer.quantityAvailable; @@ -888,7 +927,7 @@ describe("IBosonExchangeHandler", function () { .connect(assistant) .transferFrom(await assistant.getAddress(), await buyer.getAddress(), tokenId); - // Offer qunantityAvailable should be decremented + // Offer quantityAvailable should be decremented [, offer] = await offerHandler.connect(rando).getOffer(offerId); assert.equal( offer.quantityAvailable.toString(), @@ -923,6 +962,62 @@ describe("IBosonExchangeHandler", function () { ).to.emit(exchangeHandler, "BuyerCommitted"); }); + it("should work on an additional collection", async function () { + // Create a new collection + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + exchangeId = await exchangeHandler.getNextExchangeId(); + exchange.offerId = offer.id.toString(); + exchange.id = exchangeId.toString(); + const tokenId = deriveTokenId(offer.id, exchangeId); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Reserve range + await offerHandler.connect(assistant).reserveRange(offer.id, offer.quantityAvailable, assistant.address); + + // expected address of the additional collection + const voucherCloneAddress = calculateContractAddress(await accountHandler.getAddress(), "2"); + bosonVoucher = await getContractAt("BosonVoucher", voucherCloneAddress); + await bosonVoucher.connect(assistant).preMint(offer.id, offer.quantityAvailable); + + // Commit to preminted offer, retrieving the event + tx = await bosonVoucher.connect(assistant).transferFrom(assistant.address, buyer.address, tokenId); + txReceipt = await tx.wait(); + event = getEvent(txReceipt, exchangeHandler, "BuyerCommitted"); + + // Get the block timestamp of the confirmed tx + blockNumber = tx.blockNumber; + block = await provider.getBlock(blockNumber); + + // Update the committed date in the expected exchange struct with the block timestamp of the tx + voucher.committedDate = block.timestamp.toString(); + + // Update the validUntilDate date in the expected exchange struct + voucher.validUntilDate = calculateVoucherExpiry(block, voucherRedeemableFrom, voucherValid); + + // Examine event + assert.equal(event.exchangeId.toString(), exchangeId, "Exchange id is incorrect"); + assert.equal(event.offerId.toString(), offer.id, "Offer id is incorrect"); + assert.equal(event.buyerId.toString(), buyerId, "Buyer id is incorrect"); + + // Examine the exchange struct + assert.equal( + Exchange.fromStruct(event.exchange).toString(), + exchange.toString(), + "Exchange struct is incorrect" + ); + + // Examine the voucher struct + assert.equal(Voucher.fromStruct(event.voucher).toString(), voucher.toString(), "Voucher struct is incorrect"); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -1780,6 +1875,34 @@ describe("IBosonExchangeHandler", function () { assert.equal(response, ExchangeState.Revoked, "Exchange state is incorrect"); }); + it("should work on an additional collection", async function () { + // Create a new collection + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + exchange.id = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offer.id, exchange.id); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // expected address of the first additional collection + const additionalCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), "2"); + const additionalCollection = await getContractAt("BosonVoucher", additionalCollectionAddress); + + // Revoke the voucher, expecting event + await expect(exchangeHandler.connect(assistant).revokeVoucher(exchange.id)) + .to.emit(additionalCollection, "Transfer") + .withArgs(buyer.address, ZeroAddress, tokenId); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -1859,6 +1982,34 @@ describe("IBosonExchangeHandler", function () { assert.equal(response, ExchangeState.Canceled, "Exchange state is incorrect"); }); + it("should work on an additional collection", async function () { + // Create a new collection + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + exchange.id = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offer.id, exchange.id); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // expected address of the first additional collection + const additionalCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), "2"); + const additionalCollection = await getContractAt("BosonVoucher", additionalCollectionAddress); + + // Cancel the voucher, expecting event + await expect(exchangeHandler.connect(buyer).cancelVoucher(exchange.id)) + .to.emit(additionalCollection, "Transfer") + .withArgs(buyer.address, ZeroAddress, tokenId); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -2091,6 +2242,37 @@ describe("IBosonExchangeHandler", function () { ); }); + it("should work on an additional collection", async function () { + // Create a new collection + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + exchange.id = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offer.id, exchange.id); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // expected address of the first additional collection + const additionalCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), "2"); + const additionalCollection = await getContractAt("BosonVoucher", additionalCollectionAddress); + + // Set time forward to the offer's voucherRedeemableFrom + await setNextBlockTimestamp(Number(voucherRedeemableFrom)); + + // Redeem the voucher, expecting event + await expect(exchangeHandler.connect(buyer).redeemVoucher(exchange.id)) + .to.emit(additionalCollection, "Transfer") + .withArgs(buyer.address, ZeroAddress, tokenId); + }); + context("💔 Revert Reasons", async function () { it("The exchanges region of protocol is paused", async function () { // Pause the exchanges region of the protocol @@ -3800,6 +3982,35 @@ describe("IBosonExchangeHandler", function () { ).to.not.emit(exchangeHandler, "VoucherTransferred"); }); + it("should work with additional collections", async function () { + // Create a new collection + const externalId = `Brand1`; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + offer.collectionIndex = 1; + offer.id = await offerHandler.getNextOfferId(); + exchange.id = await exchangeHandler.getNextExchangeId(); + bosonVoucherCloneAddress = calculateContractAddress(await exchangeHandler.getAddress(), "2"); + bosonVoucherClone = await getContractAt("IBosonVoucher", bosonVoucherCloneAddress); + const tokenId = deriveTokenId(offer.id, exchange.id); + + // Create the offer + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + + // Commit to offer, creating a new exchange + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id, { value: price }); + + // Get the next buyer id + nextAccountId = await accountHandler.connect(rando).getNextAccountId(); + + // Call onVoucherTransferred, expecting event + await expect(bosonVoucherClone.connect(buyer).transferFrom(buyer.address, newOwner.address, tokenId)) + .to.emit(exchangeHandler, "VoucherTransferred") + .withArgs(offer.id, exchange.id, nextAccountId, await bosonVoucherClone.getAddress()); + }); + context("💔 Revert Reasons", async function () { it("The buyers region of protocol is paused", async function () { // Pause the buyers region of the protocol @@ -3891,7 +4102,7 @@ describe("IBosonExchangeHandler", function () { context("👍 undisputed exchange", async function () { it("should return false if exchange does not exists", async function () { let exchangeId = "100"; - // Invalied exchange id, ask if exchange is finalized + // Invalid exchange id, ask if exchange is finalized [exists, response] = await exchangeHandler.connect(rando).isExchangeFinalized(exchangeId); // It should not be exist diff --git a/test/protocol/MetaTransactionsHandlerTest.js b/test/protocol/MetaTransactionsHandlerTest.js index 0289ef260..76e30031e 100644 --- a/test/protocol/MetaTransactionsHandlerTest.js +++ b/test/protocol/MetaTransactionsHandlerTest.js @@ -3192,7 +3192,7 @@ describe("IBosonMetaTransactionsHandler", function () { message.from = await assistant.getAddress(); message.contractAddress = await offerHandler.getAddress(); message.functionName = - "createOffer((uint256,uint256,uint256,uint256,uint256,uint256,address,string,string,bool),(uint256,uint256,uint256,uint256),(uint256,uint256,uint256),uint256,uint256)"; + "createOffer((uint256,uint256,uint256,uint256,uint256,uint256,address,string,string,bool,uint256),(uint256,uint256,uint256,uint256),(uint256,uint256,uint256),uint256,uint256)"; message.functionSignature = functionSignature; }); diff --git a/test/protocol/OfferHandlerTest.js b/test/protocol/OfferHandlerTest.js index 6fb0b1aa3..0e0020791 100644 --- a/test/protocol/OfferHandlerTest.js +++ b/test/protocol/OfferHandlerTest.js @@ -158,6 +158,8 @@ describe("IBosonOfferHandler", function () { // All supported methods - single offer context("📋 Offer Handler Methods", async function () { beforeEach(async function () { + accountId.next(true); + // create a seller // Required constructor params id = nextAccountId = "1"; // argument sent to contract for createSeller will be ignored @@ -533,6 +535,66 @@ describe("IBosonOfferHandler", function () { ).to.emit(offerHandler, "OfferCreated"); }); + context("Additional collections", async function () { + let expectedCollectionAddress; + + beforeEach(async function () { + const externalId = "Brand1"; + expectedCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), "2"); + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + // Update collection index + offer.collectionIndex = "1"; + }); + + it("Create offer", async function () { + // Create an offer, testing for the event + await expect( + offerHandler.connect(assistant).createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId) + ) + .to.emit(offerHandler, "OfferCreated") + .withArgs( + nextOfferId, + offer.sellerId, + offer.toStruct(), + offerDatesStruct, + offerDurationsStruct, + disputeResolutionTermsStruct, + offerFeesStruct, + agentId, + assistant.address + ); + }); + + it("Reserve range", async function () { + offer.quantityAvailable = "200"; + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId); + + // expected address of the first clone + const bosonVoucher = await getContractAt("BosonVoucher", expectedCollectionAddress); + + const length = 100; + const exchangeId = "1"; + const lastExchangeId = BigInt(exchangeId) + BigInt(length) - 1n; + const firstTokenId = deriveTokenId(nextOfferId, exchangeId); + + const range = new Range(firstTokenId.toString(), length.toString(), "0", "0", assistant.address); + + // Reserve a range, testing for the event + const tx = await offerHandler.connect(assistant).reserveRange(id, length, assistant.address); + + await expect(tx) + .to.emit(offerHandler, "RangeReserved") + .withArgs(nextOfferId, offer.sellerId, exchangeId, lastExchangeId, assistant.address, assistant.address); + + await expect(tx).to.emit(bosonVoucher, "RangeReserved").withArgs(nextOfferId, range.toStruct()); + }); + }); + context("💔 Revert Reasons", async function () { it("The offers region of protocol is paused", async function () { // Pause the offers region of the protocol @@ -804,6 +866,28 @@ describe("IBosonOfferHandler", function () { offerHandler.connect(assistant).createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId) ).to.revertedWith(RevertReasons.DR_UNSUPPORTED_FEE); }); + + it("Collection does not exist", async function () { + // Set non existent collection index + offer.collectionIndex = "1"; + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler.connect(assistant).createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + + // Create a new collection + const externalId = "Brand1"; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + // Set non existent collection index + offer.collectionIndex = "2"; + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler.connect(assistant).createOffer(offer, offerDates, offerDurations, disputeResolver.id, agentId) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + }); }); context("When offer has non zero agent id", async function () { @@ -2566,6 +2650,32 @@ describe("IBosonOfferHandler", function () { .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds) ).to.revertedWith(RevertReasons.ARRAY_LENGTH_MISMATCH); }); + + it("For some offer, collection does not exist", async function () { + // Set non existent collection index + offers[3].collectionIndex = "1"; + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler + .connect(assistant) + .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + + // Create a new collection + const externalId = "Brand1"; + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + // Index "1" exists now, but "2" does not + offers[3].collectionIndex = "2"; + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler + .connect(assistant) + .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + }); }); context("When offers have non zero agent ids", async function () { diff --git a/test/protocol/OrchestrationHandlerTest.js b/test/protocol/OrchestrationHandlerTest.js index 68058680a..1818f48c1 100644 --- a/test/protocol/OrchestrationHandlerTest.js +++ b/test/protocol/OrchestrationHandlerTest.js @@ -742,16 +742,26 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedDisputeResolutionTermsStruct[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(await assistant.getAddress(), "Wrong voucher clone owner"); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -775,11 +785,21 @@ describe("IBosonOrchestrationHandler", function () { agentId ); + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -823,11 +843,21 @@ describe("IBosonOrchestrationHandler", function () { agentId ); + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -1310,16 +1340,29 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedDisputeResolutionTermsStruct[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(await assistant.getAddress(), "Wrong voucher clone owner"); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_0", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); const returnedRange = Range.fromStruct(await bosonVoucher.getRangeByOfferId(offer.id)); assert.equal(returnedRange.toString(), range.toString(), "Range mismatch"); const availablePremints = await bosonVoucher.getAvailablePreMints(offer.id); @@ -2097,6 +2140,27 @@ describe("IBosonOrchestrationHandler", function () { ) ).to.revertedWith(RevertReasons.INVALID_RANGE_LENGTH); }); + + it("Collection does not exist", async function () { + // Set inexistent collection index + offer.collectionIndex = "1"; + + // Attempt to create a seller and an offer, expecting revert + await expect( + orchestrationHandler + .connect(assistant) + .createSellerAndOffer( + seller, + offer, + offerDates, + offerDurations, + disputeResolver.id, + emptyAuthToken, + voucherInitValues, + agentId + ) + ).to.revertedWith(RevertReasons.NO_SUCH_COLLECTION); + }); }); context("When offers have non zero agent ids", async function () { @@ -5434,16 +5498,26 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedCondition[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(await assistant.getAddress(), "Wrong voucher clone owner"); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -5466,12 +5540,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -5514,12 +5598,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -5867,16 +5961,29 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedCondition[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(await assistant.getAddress(), "Wrong voucher clone owner"); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_0", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); const returnedRange = Range.fromStruct(await bosonVoucher.getRangeByOfferId(offer.id)); assert.equal(returnedRange.toString(), range.toString(), "Range mismatch"); const availablePremints = await bosonVoucher.getAvailablePreMints(offer.id); @@ -6195,16 +6302,26 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedBundle[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(await assistant.getAddress(), "Wrong voucher clone owner"); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -6230,12 +6347,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -6281,12 +6408,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -6705,16 +6842,29 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedBundle[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(await assistant.getAddress(), "Wrong voucher clone owner"); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_0", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); const returnedRange = Range.fromStruct(await bosonVoucher.getRangeByOfferId(offer.id)); assert.equal(returnedRange.toString(), range.toString(), "Range mismatch"); const availablePremints = await bosonVoucher.getAvailablePreMints(offer.id); @@ -7102,16 +7252,26 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedBundle[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(await assistant.getAddress(), "Wrong voucher clone owner"); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -7138,12 +7298,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -7190,12 +7360,22 @@ describe("IBosonOrchestrationHandler", function () { agentId ); - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -7664,16 +7844,29 @@ describe("IBosonOrchestrationHandler", function () { expect(JSON.stringify(returnedBundle[key]) === JSON.stringify(value)).is.true; } - // Voucher clone contract + // Get the collections information expectedCloneAddress = calculateContractAddress(await orchestrationHandler.getAddress(), "1"); - bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCloneAddress); expect(await bosonVoucher.owner()).to.equal(await assistant.getAddress(), "Wrong voucher clone owner"); bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_0", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); const returnedRange = Range.fromStruct(await bosonVoucher.getRangeByOfferId(offer.id)); assert.equal(returnedRange.toString(), range.toString(), "Range mismatch"); const availablePremints = await bosonVoucher.getAvailablePreMints(offer.id); diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index 343b6f762..865b7ade9 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -11,6 +11,7 @@ const { calculateContractAddress, setupTestEnvironment, getSnapshot, revertToSna const { VOUCHER_NAME, VOUCHER_SYMBOL } = require("../util/constants"); const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens"); const { mockSeller, mockAuthToken, mockVoucherInitValues, accountId } = require("../util/mock"); +const { Collection, CollectionList } = require("../../scripts/domain/Collection"); /** * Test the Boson Seller Handler @@ -224,6 +225,13 @@ describe("SellerHandler", function () { expect(JSON.stringify(returnedAuthToken[key]) === JSON.stringify(value)).is.true; } + // Get the collections information + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + // Voucher clone contract bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); @@ -231,8 +239,11 @@ describe("SellerHandler", function () { bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should update state when voucherInitValues has zero royaltyPercentage and exchangeId does not exist", async function () { @@ -245,8 +256,11 @@ describe("SellerHandler", function () { bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -279,8 +293,11 @@ describe("SellerHandler", function () { bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); // Prepare random parameters let exchangeId = "1234"; // An exchange id that does not exist @@ -326,6 +343,13 @@ describe("SellerHandler", function () { expect(JSON.stringify(returnedAuthToken[key]) === JSON.stringify(value)).is.true; } + // Get the collections information + const [defaultVoucherAddress, additionalCollections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + expect(defaultVoucherAddress).to.equal(expectedCloneAddress, "Wrong default voucher address"); + expect(additionalCollections.length).to.equal(0, "Wrong number of additional collections"); + // Voucher clone contract bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); @@ -333,8 +357,11 @@ describe("SellerHandler", function () { bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); - expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id, "Wrong voucher client name"); - expect(await bosonVoucher.symbol()).to.equal(VOUCHER_SYMBOL + "_" + seller.id, "Wrong voucher client symbol"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_0", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_0", + "Wrong voucher client symbol" + ); }); it("should ignore any provided id and assign the next available", async function () { @@ -2499,6 +2526,56 @@ describe("SellerHandler", function () { ).to.not.emit(accountHandler, "SellerUpdateApplied"); }); + it("Transfers the ownerships of the default boson voucher.", async function () { + const expectedDefaultAddress = calculateContractAddress(await accountHandler.getAddress(), "1"); // default + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedDefaultAddress); + + // original voucher contract owner + expect(await bosonVoucher.owner()).to.equal(assistant.address); + + seller.assistant = other1.address; + sellerStruct = seller.toStruct(); + + await accountHandler.connect(admin).updateSeller(seller, emptyAuthToken); + await accountHandler.connect(other1).optInToSellerUpdate(seller.id, [SellerUpdateFields.Assistant]); + + // new voucher contract owner + expect(await bosonVoucher.owner()).to.equal(other1.address); + }); + + it("Transfers ownerships of all additional collections", async function () { + const expectedDefaultAddress = calculateContractAddress(await accountHandler.getAddress(), "1"); // default + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedDefaultAddress); + + const additionalCollections = []; + // create 3 additional collections + for (let i = 0; i < 3; i++) { + const externalId = `Brand${i}`; + voucherInitValues.contractURI = `https://brand${i}.com`; + const expectedCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), i + 2); + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + additionalCollections.push(await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress)); + } + + // original voucher and collections contract owner + expect(await bosonVoucher.owner()).to.equal(assistant.address); + for (const collection of additionalCollections) { + expect(await collection.owner()).to.equal(assistant.address); + } + + seller.assistant = other1.address; + sellerStruct = seller.toStruct(); + + await accountHandler.connect(admin).updateSeller(seller, emptyAuthToken); + await accountHandler.connect(other1).optInToSellerUpdate(seller.id, [SellerUpdateFields.Assistant]); + + // new voucher and collections contract owner + expect(await bosonVoucher.owner()).to.equal(other1.address); + for (const collection of additionalCollections) { + expect(await collection.owner()).to.equal(other1.address); + } + }); + context("💔 Revert Reasons", async function () { it("There are no pending updates", async function () { seller.admin = await other1.getAddress(); @@ -2655,5 +2732,205 @@ describe("SellerHandler", function () { }); }); }); + + context("👉 createNewCollection()", async function () { + let externalId, expectedDefaultAddress, expectedCollectionAddress; + let royaltyPercentage; + + beforeEach(async function () { + // Create a seller + await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); + + externalId = "Brand1"; + voucherInitValues.contractURI = contractURI = "https://brand1.com"; + voucherInitValues.royaltyPercentage = royaltyPercentage = "100"; // 1% + expectedDefaultAddress = calculateContractAddress(await accountHandler.getAddress(), "1"); // default + expectedCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), "2"); + }); + + it("should emit a CollectionCreated event", async function () { + // Create a new collection, testing for the event + const tx = await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + await expect(tx) + .to.emit(accountHandler, "CollectionCreated") + .withArgs(seller.id, 1, expectedCollectionAddress, externalId, assistant.address); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + + await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); + await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs(royaltyPercentage); + await expect(tx) + .to.emit(bosonVoucher, "VoucherInitialized") + .withArgs(seller.id, royaltyPercentage, contractURI); + + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + await expect(tx).to.emit(bosonVoucher, "OwnershipTransferred").withArgs(ZeroAddress, assistant.address); + }); + + it("should update state", async function () { + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + const expectedCollections = new CollectionList([new Collection(expectedCollectionAddress, externalId)]); + + // Get the collections information + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); + + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); + expect(await bosonVoucher.name()).to.equal(VOUCHER_NAME + " " + seller.id + "_1", "Wrong voucher client name"); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_1", + "Wrong voucher client symbol" + ); + }); + + it("create multiple collections", async function () { + const expectedCollections = new CollectionList([]); + + for (let i = 1; i < 4; i++) { + expectedCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), (i + 1).toString()); + externalId = `Brand${i}`; + voucherInitValues.contractURI = contractURI = `https://brand${i}.com`; + voucherInitValues.royaltyPercentage = royaltyPercentage = (i * 100).toString(); // 1%, 2%, 3% + + // Create a new collection, testing for the event + const tx = await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + await expect(tx) + .to.emit(accountHandler, "CollectionCreated") + .withArgs(seller.id, i, expectedCollectionAddress, externalId, assistant.address); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + + await expect(tx).to.emit(bosonVoucher, "ContractURIChanged").withArgs(contractURI); + await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged").withArgs(royaltyPercentage); + await expect(tx) + .to.emit(bosonVoucher, "VoucherInitialized") + .withArgs(seller.id, royaltyPercentage, contractURI); + + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + await expect(tx).to.emit(bosonVoucher, "OwnershipTransferred").withArgs(ZeroAddress, assistant.address); + + // Get the collections information + expectedCollections.collections.push(new Collection(expectedCollectionAddress, externalId)); + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + + // Voucher clone contract + bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); + + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + expect(await bosonVoucher.contractURI()).to.equal(contractURI, "Wrong contract URI"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " " + seller.id + "_" + i, + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_" + seller.id + "_" + i, + "Wrong voucher client symbol" + ); + } + }); + + context("💔 Revert Reasons", async function () { + it("The sellers region of protocol is paused", async function () { + // Pause the sellers region of the protocol + await pauseHandler.connect(pauser).pause([PausableRegion.Sellers]); + + // Attempt to create a new collection expecting revert + await expect( + accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues) + ).to.revertedWith(RevertReasons.REGION_PAUSED); + }); + + it("Caller is not anyone's assistant", async function () { + // Attempt to create a new collection + await expect( + accountHandler.connect(rando).createNewCollection(externalId, voucherInitValues) + ).to.revertedWith(RevertReasons.NO_SUCH_SELLER); + }); + }); + }); + + context("👉 getSellersCollections()", async function () { + let externalId, expectedDefaultAddress, expectedCollectionAddress; + + beforeEach(async function () { + // Create a seller + await accountHandler.connect(admin).createSeller(seller, emptyAuthToken, voucherInitValues); + + expectedDefaultAddress = calculateContractAddress(await accountHandler.getAddress(), "1"); // default + }); + + it("should return a default voucher address and an empty collections list if seller does not have any", async function () { + const expectedCollections = new CollectionList([]); + + // Get the collections information + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + + it("should return correct collection list", async function () { + const expectedCollections = new CollectionList([]); + + for (let i = 1; i < 4; i++) { + expectedCollectionAddress = calculateContractAddress(await accountHandler.getAddress(), (i + 1).toString()); + externalId = `Brand${i}`; + voucherInitValues.contractURI = `https://brand${i}.com`; + + // Create a new collection + await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + // Add to expected collections + expectedCollections.collections.push(new Collection(expectedCollectionAddress, externalId)); + } + + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + + it("should return zero values if seller does not exist ", async function () { + const sellerId = 777; + const expectedCollections = new CollectionList([]); + + // Get the collections information + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(sellerId); + const additionalCollections = CollectionList.fromStruct(collections); + expect(defaultVoucherAddress).to.equal(ZeroAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + }); }); }); diff --git a/test/protocol/clients/BosonVoucherTest.js b/test/protocol/clients/BosonVoucherTest.js index 4c9cf9103..351ded014 100644 --- a/test/protocol/clients/BosonVoucherTest.js +++ b/test/protocol/clients/BosonVoucherTest.js @@ -103,7 +103,7 @@ describe("IBosonVoucher", function () { voucherInitValues = mockVoucherInitValues(); const bosonVoucherInit = await getContractAt("BosonVoucher", await bosonVoucher.getAddress()); - await bosonVoucherInit.initializeVoucher(sellerId, await assistant.getAddress(), voucherInitValues); + await bosonVoucherInit.initializeVoucher(sellerId, "1", await assistant.getAddress(), voucherInitValues); [foreign20] = await deployMockTokens(["Foreign20", "BosonToken"]); @@ -151,9 +151,12 @@ describe("IBosonVoucher", function () { }); it("Cannot initialize voucher twice", async function () { - const initalizableClone = await getContractAt("IInitializableVoucherClone", await bosonVoucher.getAddress()); + const initalizableClone = await ethers.getContractAt( + "IInitializableVoucherClone", + await bosonVoucher.getAddress() + ); await expect( - initalizableClone.initializeVoucher(2, await assistant.getAddress(), voucherInitValues) + initalizableClone.initializeVoucher(2, "1", await assistant.getAddress(), voucherInitValues) ).to.be.revertedWith(RevertReasons.INITIALIZABLE_ALREADY_INITIALIZED); }); }); diff --git a/test/util/mock.js b/test/util/mock.js index 6f3b78850..8d5c73911 100644 --- a/test/util/mock.js +++ b/test/util/mock.js @@ -75,6 +75,7 @@ async function mockOffer() { const metadataHash = "QmYXc12ov6F2MZVZwPs5XeCBbf61cW3wKRk8h3D5NTYj4T"; // not an actual metadataHash, just some data for tests const metadataUri = `https://ipfs.io/ipfs/${metadataHash}`; const voided = false; + const collectionIndex = "0"; // Create a valid offer, then set fields in tests directly let offer = new Offer( @@ -87,7 +88,8 @@ async function mockOffer() { exchangeToken, metadataUri, metadataHash, - voided + voided, + collectionIndex ); const offerDates = await mockOfferDates();