Smart Contract
FlowtyRentals
A.5c57f79c6694797f.FlowtyRentals
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import LostAndFound from 0x473d6a2c37eab5be
5import RoyaltiesLedger from 0x5c57f79c6694797f
6import FlowtyUtils from 0x3cdbb3d569211ff3
7import Flowty from 0x5c57f79c6694797f
8import FlowtyListingCallback from 0x3cdbb3d569211ff3
9import DNAHandler from 0x3cdbb3d569211ff3
10import Burner from 0xf233dcee88fe0abe
11
12// FlowtyRentals
13//
14// A smart contract responsible for the letting accounts take temporary ownership
15// of assets. It allows owners to list items they own for a fee + deposit and a term
16// that other accounts can then rent. If the item being rented is returned before
17// the term is up, their deposit is returned. If, however, they do not return the rented
18// item by this time, they might lose their deposit if settlement cannot facilitate
19// automatically returning the asset.
20//
21// Each account that wants to list a an item for rent installs a Storefront.
22// Storefronts can list items for rent. There is one Storefront per account,
23// it handles rentals of all NFT types for that account.
24//
25// Each NFT may be listed in one or more Listings, the validity of each
26// Listing can easily be checked.
27//
28// Lenders can watch for Listing events and check the NFT type and
29// ID to see if they wish to rent the listing on a storefront.
30access(all) contract FlowtyRentals {
31 access(all) entitlement List
32 access(all) entitlement Cancel
33 access(all) entitlement Administrator
34
35
36 // FlowtyRentalsInitialized
37 // This contract has been deployed
38 access(all) event FlowtyRentalsInitialized()
39
40 // FlowtyRentalsStorefrontInitialized
41 // A FlowtyRentalsStorefront resource has been created.
42 // Event consumers can now expect events from this FlowtyRentalsStorefront.
43 // Note that we do not specify an address: we cannot and should not.
44 // Created resources do not have an owner address, and may be moved
45 // after creation in ways we cannot check.
46 // ListingAvailable events can be used to determine the address
47 // of the owner of the FlowtyRentalsStorefront (...its location) at the time of
48 // the listing but only at that precise moment in that precise transaction.
49 // If the seller moves the FlowtyRentalsStorefront while the listing is valid,
50 // that is on them.
51 //
52 access(all) event FlowtyRentalsStorefrontInitialized(flowtyRentalsStorefrontResourceID: UInt64)
53
54 // FlowtyRentalsStorefrontfrontDestroyed
55 // A FlowtyRentalsStorefront has been destroyed.
56 // Event consumers can now stop processing events from this FlowtyRentalsStorefront.
57 // Note that we do not specify an address.
58 //
59 access(all) event FlowtyRentalsStorefrontDestroyed(flowtyRentalsStorefrontResourceID: UInt64)
60
61 // FlowtyRentalsMarketplaceInitialized
62 // A FlowtyRentalsMarketplace resource has been created.
63 // Event consumers can now expect events from this FlowtyRentalsMarketplace.
64 // Note that we do not specify an address: we cannot and should not.
65 // Created resources do not have an owner address, and may be moved
66 // after creation in ways we cannot check.
67 // While additional FlowtyRentalsMarketplace resources can be made, this contract will,
68 // by default, access the one stored in this contract address's account. Any listing that is
69 // funded will get routed to and stored there.
70 //
71 access(all) event FlowtyRentalsMarketplaceInitialized(flowtyRentalsMarketplaceResourceID: UInt64)
72
73 // FlowtyRentalsMarketplaceDestroyed
74 // A FlowtyRentalsMarketplace has been destroyed.
75 // Event consumers can now stop processing events from this FlowtyRentalsMarketplace.
76 // Note that we do not specify an address.
77 //
78 access(all) event FlowtyRentalsMarketplaceDestroyed(flowtyRentalsMarketplaceResourceID: UInt64)
79
80 // ListingAvailable
81 // A listing has been created and added to a FlowtyRentalsStorefront resource.
82 // The address values here are valid when the event is emitted, but
83 // the state of the accounts they refer to may be changed outside of the
84 // FlowtyRentalsStorefront workflow, so be careful to check before using them.
85 //
86 access(all) event ListingAvailable(
87 flowtyStorefrontAddress: Address,
88 flowtyStorefrontID: UInt64,
89 listingResourceID: UInt64,
90 nftType: String,
91 nftID: UInt64,
92 amount: UFix64,
93 deposit: UFix64,
94 term: UFix64,
95 royaltyRate: UFix64,
96 expiresAfter: UFix64,
97 paymentTokenType: String,
98 renter: Address?
99 )
100
101 // ListingRented
102 // A Rental has been created and added to a FlowtyRentalsStorefront resource
103 // The address values here are valid when an event is emitted, but the state
104 // of the accounts they refer to may be changed outside of the
105 // FlowtyRentalsMarketplace workflow, so be careful to check when using them.
106 //
107 access(all) event ListingRented(
108 flowtyStorefrontAddress: Address,
109 flowtyStorefrontID: UInt64,
110 renterAddress: Address,
111 listingResourceID: UInt64,
112 rentalResourceID: UInt64,
113 nftID: UInt64,
114 nftType: String,
115 amount: UFix64,
116 deposit: UFix64,
117 enabledAutomaticReturn: Bool
118 )
119
120 // RentalReturned
121 // A rental has been returned, releasing the deposit back to its renter
122 //
123 access(all) event RentalReturned(
124 flowtyStorefrontAddress: Address,
125 flowtyStorefrontID: UInt64,
126 renterAddress: Address,
127 listingResourceID: UInt64,
128 rentalResourceID: UInt64,
129 nftID: UInt64,
130 nftType: String
131 )
132
133 // ListingDestroyed
134 // Listing has been destroyed and has been removed from
135 // the owning account's FlowtyRentalsStorefront
136 //
137 access(all) event ListingDestroyed(
138 flowtyStorefrontAddress: Address,
139 flowtyStorefrontID: UInt64,
140 listingResourceID: UInt64,
141 nftID: UInt64,
142 nftType: String
143 )
144
145 // RentalSettled
146 // This Rental was not returned in time, its deposit has been distributed
147 // to the original owner of the rented asset and to the assets'
148 // royalty beneficiaries
149 //
150 access(all) event RentalSettled(
151 rentalResourceID: UInt64,
152 listingResourceID: UInt64,
153 renter: Address,
154 lender: Address,
155 nftID: UInt64,
156 nftType: String,
157 deposit: UFix64
158 )
159
160 access(all) let FlowtyRentalsStorefrontStoragePath: StoragePath
161 access(all) let FlowtyRentalsMarketplaceStoragePath: StoragePath
162 access(all) let FlowtyRentalsStorefrontPublicPath: PublicPath
163 access(all) let FlowtyRentalsMarketplacePublicPath: PublicPath
164 access(all) let FlowtyRentalsAdminStoragePath: StoragePath
165
166 // SuspendedFundingPeriod
167 // A period of time in seconds before which
168 // a listing cannot be rented.
169 access(all) var SuspendedFundingPeriod: UFix64
170
171 // Fee
172 // Fee taken as a percentage of the initial rental fee.
173 // For instance, if a rental has a fee of 10, Flowty will take a fee
174 // of 10 * Fee where Fee is less than 1.
175 access(all) var Fee: UFix64
176
177 // ListingDetails
178 // Non-resource data about a listing.
179 //
180 access(all) struct ListingDetails {
181 // The ID of the storefront that this listing was made on.
182 // Note that this listing cannot be transferred to another address
183 access(all) var flowtyStorefrontID: UInt64
184
185 // Signal indicating whether the listing has been rented or not.
186 // It can only be rented once.
187 access(all) var rented: Bool
188
189 // The type of the nft being rented. When attempting to rent an nft,
190 // a listing will check that the type of the nft being rented
191 // matches nftType
192 //
193 access(all) let nftType: Type
194
195 // The ID of the nft being rented. When attempting to rent an nft,
196 // a listing will check that the ID of the nft being rented
197 // matched nftID
198 access(all) let nftID: UInt64
199
200 // The flat fee amount collected. This amount is split when a listing is
201 // rented to the owner of the listing, flowty, and royalty owners
202 //
203 access(all) let amount: UFix64
204
205 // The deposit fee collected. This amount is returned to a renter
206 // if they return the nft they rented. However, if they do not,
207 // the deposit will be transferred to the original owner of the nft
208 // and to the royalty recipient of the nft collection
209 //
210 access(all) let deposit: UFix64
211
212 // The number of seconds that a rental is valid for when it is rented
213 // After the term has elapsed, a rental can be settled.
214 access(all) var term: UFix64
215
216 // The type of FungibleToken to be accepted as payment.
217 access(all) let paymentVaultType: Type
218
219 // A flag to reenable the listing once it has been returned
220 // This allows a user to keep a listing open in perpetuity
221 // should they desire to leave it open.
222 access(all) var reenableOnReturn: Bool
223
224 // Contains information about how to pay owner of this listing
225 access(self) let paymentCut: Flowty.PaymentCut
226
227 // The time that this listing was created
228 access(all) let listedTime: UFix64
229
230 // The royalty percentage taken at the time of listing.
231 access(all) var royaltyRate: UFix64
232
233 // The duration that this listing is valid for. If this amount of time
234 // has passed, the listing can no-longer be rented.
235 access(all) var expiresAfter: UFix64
236
237 // An optional parameter that can be used to prevent all addresses
238 // except for the specified one to rent this listing.
239 // This is how we achieve private listings.
240 access(all) var renter: Address?
241
242 access(all) view fun getPaymentCut(): Flowty.PaymentCut {
243 return self.paymentCut
244 }
245
246 // getTotalPayment
247 // get the total amount needed to rent this listing.
248 access(all) view fun getTotalPayment(): UFix64 {
249 return self.amount + self.deposit
250 }
251
252 access(contract) fun setToRented() {
253 self.rented = true
254 }
255
256 init (
257 nftType: Type,
258 nftID: UInt64,
259 amount: UFix64,
260 deposit: UFix64,
261 term: UFix64,
262 paymentVaultType: Type,
263 storefrontID: UInt64,
264 paymentCut: Flowty.PaymentCut,
265 expiresAfter: UFix64,
266 renter: Address?,
267 royaltyRate: UFix64
268 ) {
269 assert(paymentCut.amount > 0.0, message: "Listing must have non-zero requested amount")
270
271 self.flowtyStorefrontID = storefrontID
272 self.rented = false
273 self.nftType = nftType
274 self.nftID = nftID
275 self.amount = amount
276 self.deposit = deposit
277 self.term = term
278 self.paymentVaultType = paymentVaultType
279 self.listedTime = getCurrentBlock().timestamp
280 self.royaltyRate = royaltyRate
281 self.expiresAfter = expiresAfter
282 self.paymentCut = paymentCut
283 self.renter = renter
284 self.reenableOnReturn = false
285 }
286 }
287
288 // ListingPublic
289 // An inerface providing a useful public interface to a listing
290 //
291 access(all) resource interface ListingPublic {
292 // borrowNFT
293 // This will assert in the same way as the NFT standard borrowNFT()
294 // if the NFT is absent, for example if it has been sold via another listing.
295 //
296 access(all) view fun borrowNFT(): &{NonFungibleToken.NFT}?
297
298 // rent
299 // Rent the listing. Distributing fees to all parties and taking a deposit to be held
300 // until the nft is either returned or the rental defaults. A rental can be automatically returned
301 // if renterNFTProvider is provided, giving us a way obtain the the rented nft automatically
302 // to be returned.
303 access(all) fun rent(
304 payment: @{FungibleToken.Vault},
305 renterFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
306 renterNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
307 renterNFTProvider: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.CollectionPublic, NonFungibleToken.Provider}>?
308 )
309
310 access(all) view fun getDetails(): ListingDetails
311
312 // suspensionTimeRemaining
313 // returns the amount of time left until a listing can be filled.
314 //
315 access(all) view fun suspensionTimeRemaining(): Fix64
316
317 // remainingTimeToRent
318 // returns the amount of time left until this listing is no longer valid
319 //
320 access(all) view fun remainingTimeToRent(): Fix64
321
322 // isRentingEnabled
323 // checks if a listing can be rented or not.
324 //
325 access(all) view fun isRentingEnabled(): Bool
326 }
327
328 // Listing
329 // A resource that allows an NFT to be temporarily owned by another account in exchange
330 // for a Fee and a Deposit.
331 access(all) resource Listing: ListingPublic, FlowtyListingCallback.Listing, Burner.Burnable {
332 access(all) event ResourceDestroyed(
333 listingResourceID: UInt64 = self.uuid,
334 flowtyStorefrontID: UInt64 = self.details.flowtyStorefrontID,
335 funded: Bool = self.details.rented,
336 nftID: UInt64 = self.details.nftID,
337 nftType: String = self.details.nftType.identifier,
338 flowtyStorefrontAddress: Address = self.nftPublicCollectionCapability.address
339 )
340
341 // The simple (non-Capability, non-complex) details of the listing
342 access(self) let details: ListingDetails
343
344 // A capability allowing this resource to withdraw the NFT with the given ID from its collection.
345 // This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
346 // such a capability to a resource and always check its code to make sure it will use it in the
347 // way that it claims.
348 access(contract) let nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
349
350 // A capability allowing this resource to access the owner's NFT public collection
351 access(contract) let nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>
352
353 // reference to the owner's fungibleTokenReceiver to pay them in the event of a settlement.
354 access(contract) let ownerFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>
355
356 // borrowNFT
357 // This will assert in the same way as the NFT standard borrowNFT()
358 // if the NFT is absent, for example if it has been sold via another listing.
359 //
360 access(all) view fun borrowNFT(): &{NonFungibleToken.NFT}? {
361 pre {
362 self.nftProviderCapability.check(): "provider capability failed check"
363 }
364
365 if let ref = self.nftProviderCapability.borrow()!.borrowNFT(self.getDetails().nftID) {
366 if ref.getType() != self.details.nftType || ref.id != self.details.nftID {
367 return nil
368 }
369
370 return ref
371 }
372
373 return nil
374 }
375
376 // getDetails
377 // Get the details of the current state of the Listing as a struct.
378 // This avoids having more public variables and getter methods for them, and plays
379 // nicely with scripts (which cannot return resources).
380 //
381 access(all) view fun getDetails(): ListingDetails {
382 return self.details
383 }
384
385 // rent
386 // Rent the listing. Distributing fees to all parties and taking a deposit to be held
387 // until the nft is either returned or the rental defaults. A rental can be automatically returned
388 // if renterNFTProvider is provided, giving us a way obtain the the rented nft automatically
389 // to be returned.
390 access(all) fun rent(
391 payment: @{FungibleToken.Vault},
392 renterFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
393 renterNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
394 renterNFTProvider: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.CollectionPublic, NonFungibleToken.Provider}>?
395 ) {
396 pre {
397 self.isRentingEnabled(): "Renting is not enabled or this listing has expired"
398 !self.details.rented: "listing has already been rented"
399 payment.getType() == self.details.paymentVaultType: "payment vault is not requested fungible token"
400 payment.balance == self.details.getTotalPayment(): "payment vault does not contain requested amount"
401 self.nftProviderCapability.check(): "nftProviderCapability failed check"
402 renterNFTCollection.check(): "renterNFTCollection failed check"
403 renterNFTProvider == nil || renterNFTProvider!.check(): "renterNFTProvider failed check"
404 }
405
406 // handle if this listing is private or not.
407 if self.details.renter != nil {
408 assert(renterNFTCollection.address == self.details.renter, message: "incorrect renter address on renterNFTCollection")
409 assert(renterFungibleTokenReceiver.address == self.details.renter, message: "incorrect renter address")
410 }
411
412 self.details.setToRented()
413
414 // withdraw the NFT being rented and ensure that its type and id match the listing.
415 // This protects the renter from receiving the wrong nft.
416 let nft <- self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
417 let ref = &nft as &{NonFungibleToken.NFT}
418 assert(nft.getType() == self.details.nftType, message: "withdrawn NFT is not of specified type")
419 assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
420 assert(FlowtyUtils.isSupported(ref), message: "nft type is not supported")
421
422 let royalties = RoyaltiesLedger.get(self.uuid)
423
424 // set the deposit aside and calculate fee payouts for the rest
425 let depositCut <- payment.withdraw(amount: self.details.deposit)
426 assert(payment.balance == self.details.amount, message: "balance after deposit does not match rental amount")
427
428 // create the Rental resource on the Flowty accounts' marketplace
429 let listingResourceID = self.uuid
430 let marketplace = FlowtyRentals.borrowMarketplace()
431 let rental = marketplace.createRental(
432 storefrontID: self.details.flowtyStorefrontID,
433 listingResourceID: listingResourceID,
434 nftID: self.details.nftID,
435 nftType: self.details.nftType,
436 paymentVaultType: self.details.paymentVaultType,
437 term: self.details.term,
438 listingDetails: self.details,
439 ownerNFTCollectionPublic: self.nftPublicCollectionCapability,
440 ownerFungibleTokenReceiver: self.ownerFungibleTokenReceiver,
441 renterFungibleTokenReceiver: renterFungibleTokenReceiver,
442 depositedFungibleTokens: <-depositCut,
443 renterNFTCollection: renterNFTCollection,
444 renterNFTProvider: renterNFTProvider
445 )
446
447 let details = rental.getDetails()
448 let expiration = details.term + details.startTime
449 // September 3rd 12:00 AM
450 assert(payment.getType().identifier != "A.b19436aae4d94622.FiatToken.Vault" || expiration < 1725321600.0, message: "rentals are not able to be filled that expire on or after September 3rd 12:00 AM GMT.")
451
452 if let callback = FlowtyRentals.borrowCallbackContainer() {
453 callback.handle(stage: FlowtyListingCallback.Stage.Filled, listing: &self as &{FlowtyListingCallback.Listing}, nft: ref)
454 callback.handle(stage: FlowtyListingCallback.Stage.Created, listing: rental, nft: ref)
455 }
456
457 // transfer the nft being rented into the renter's nft collection
458 let renterCollectionCap = renterNFTCollection.borrow()!
459 renterCollectionCap.deposit(token: <-nft)
460
461 // Calculate fee amounts. First we calculate the amount sent to Flowty
462 // and to the royalty recipient. The remainder goes to the listing owner.
463 let flowtyFeeAmount = payment.balance * FlowtyRentals.Fee
464 let royaltyFeeAmount = payment.balance * self.details.royaltyRate
465
466 // withdraw the cuts going to flowty and our royalty recipient into their own vaults
467 let flowtyFeeCut <- payment.withdraw(amount: flowtyFeeAmount)
468 let royaltyFeeCut <- payment.withdraw(amount: royaltyFeeAmount)
469
470 let tokenInfo = FlowtyUtils.getTokenInfo(self.details.paymentVaultType)!
471 let royaltyTokenPath = tokenInfo.receiverPath
472 let depositor = FlowtyRentals.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
473
474 if royalties != nil {
475 let tokenInfo = FlowtyUtils.getTokenInfo(self.details.paymentVaultType)!
476 let royaltyCuts = FlowtyUtils.metadataRoyaltiesToRoyaltyCuts(tokenInfo: tokenInfo, mdRoyalties: [royalties!])
477 if let leftover <- FlowtyUtils.distributeRoyaltiesWithDepositor(royaltyCuts: royaltyCuts, depositor: depositor, vault: <-royaltyFeeCut) {
478 payment.deposit(from: <-leftover)
479 }
480 } else {
481 payment.deposit(from: <-royaltyFeeCut)
482 }
483
484 // get payment cut information for the listing owner to receive payment for
485 // this listing being rented.
486 assert(self.details.getPaymentCut().receiver.check(), message: "paymentCut receiver failed check")
487 let receiver = self.details.getPaymentCut().receiver.borrow()!
488 receiver.deposit(from: <-payment)
489
490 // get the path and corresponding capability to send fees to the Flowty account
491 let flowtyFeeReceiver = FlowtyRentals.account.capabilities.get<&{FungibleToken.Receiver}>(tokenInfo.receiverPath)!.borrow()!
492 flowtyFeeReceiver.deposit(from: <-flowtyFeeCut)
493
494 // check if automatic return has been enabled by renter.
495 let enabledAutomaticReturn = renterNFTProvider != nil
496
497 emit ListingRented(
498 flowtyStorefrontAddress: self.owner!.address,
499 flowtyStorefrontID: self.details.flowtyStorefrontID,
500 renterAddress: renterNFTCollection.address,
501 listingResourceID: listingResourceID,
502 rentalResourceID: rental.uuid,
503 nftID: self.details.nftID,
504 nftType: self.details.nftType.identifier,
505 amount: self.details.amount,
506 deposit: self.details.deposit,
507 enabledAutomaticReturn: enabledAutomaticReturn
508 )
509 }
510
511 // suspensionTimeRemaining
512 // returns the amount of time left until a listing can be filled.
513 //
514 access(all) view fun suspensionTimeRemaining() : Fix64 {
515 let listedTime = self.details.listedTime
516 let currentTime = getCurrentBlock().timestamp
517
518 let remaining = Fix64(listedTime + Flowty.SuspendedFundingPeriod) - Fix64(currentTime)
519
520 return remaining
521 }
522
523 // remainingTimeToRent
524 // returns the amount of time left until this listing is no longer valid
525 //
526 access(all) view fun remainingTimeToRent(): Fix64 {
527 let listedTime = self.details.listedTime
528 let currentTime = getCurrentBlock().timestamp
529 let remaining = Fix64(listedTime + self.details.expiresAfter) - Fix64(currentTime)
530 return remaining
531 }
532
533 // isRentingEnabled
534 // checks if a listing can be rented or not.
535 //
536 access(all) view fun isRentingEnabled(): Bool {
537 let timeRemaining = self.suspensionTimeRemaining()
538 let listingTimeRemaining = self.remainingTimeToRent()
539 return timeRemaining < Fix64(0.0) && listingTimeRemaining > Fix64(0.0)
540 }
541
542 init (
543 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
544 nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
545 ownerFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
546 nftType: Type,
547 nftID: UInt64,
548 amount: UFix64,
549 deposit: UFix64,
550 term: UFix64,
551 paymentVaultType: Type,
552 paymentCut: Flowty.PaymentCut,
553 storefrontID: UInt64,
554 expiresAfter: UFix64,
555 renter: Address?
556 ) {
557 pre {
558 nftProviderCapability.check(): "nftProviderCapability failed check"
559 nftPublicCollectionCapability.check(): "nftPublicCollectionCapability failed check"
560 ownerFungibleTokenReceiver.check(): "ownerFungibleTokenReceiver failed check"
561 }
562
563 self.nftProviderCapability = nftProviderCapability
564 self.nftPublicCollectionCapability = nftPublicCollectionCapability
565 self.ownerFungibleTokenReceiver = ownerFungibleTokenReceiver
566
567 let provider = self.nftProviderCapability.borrow()!
568 let nft = provider.borrowNFT(nftID) ?? panic("could not borrow nft")
569 assert(nft.getType() == nftType, message: "token is not of specified type")
570 assert(nft.id == nftID, message: "token does not have specified ID")
571 let royaltyRate = FlowtyUtils.getRoyaltyRate(nft)
572
573 self.details = ListingDetails(
574 nftType: nftType,
575 nftID: nftID,
576 amount: amount,
577 deposit: deposit,
578 term: term,
579 paymentVaultType: paymentVaultType,
580 storefrontID: storefrontID,
581 paymentCut: paymentCut,
582 expiresAfter: expiresAfter,
583 renter: renter,
584 royaltyRate: royaltyRate
585 )
586
587 if let callback = FlowtyRentals.borrowCallbackContainer() {
588 callback.handle(stage: FlowtyListingCallback.Stage.Created, listing: &self as &{FlowtyListingCallback.Listing}, nft: nft)
589 }
590 }
591
592 access(contract) fun burnCallback() {
593 FlowtyRentals.borrowRoyaltiesLedger().remove(self.uuid)
594
595 emit ListingDestroyed(
596 flowtyStorefrontAddress: self.ownerFungibleTokenReceiver.address,
597 flowtyStorefrontID: self.details.flowtyStorefrontID,
598 listingResourceID: self.uuid,
599 nftID: self.details.nftID,
600 nftType: self.details.nftType.identifier
601 )
602 }
603 }
604
605 // Rental Details
606 // A struct containing a Rental's non-resource data.
607 access(all) struct RentalDetails {
608 // |-- gathered by an initialized Rental resource --|
609 // The storefront which owns this rental.
610 access(all) var flowtyStorefrontID: UInt64
611 // The listing being funded
612 access(all) var listingResourceID: UInt64
613 // They type used for payment to obtain the rental.
614 // Tokens taken as a deposit are also made of this type
615 access(all) var paymentVaultType: Type
616
617 // The number of seconds that this rental is good for.
618 // If the rental is not returned before this time, the deposit can be revoked
619 access(all) var term: UFix64
620
621 // The id of the nft that was rented. This is the same as listingDetails.nftID
622 access(all) var nftID: UInt64
623
624 // The type of the nft that was rented. This is the same as listingDetails.nftType
625 access(all) var nftType: Type
626
627 // |-- The below variables are maintained by our smart contract --|
628 access(all) var returned: Bool
629 access(all) var settled: Bool
630
631 // The time this Rental was created
632 access(all) var startTime: UFix64
633
634 access(contract) fun setToReturned() {
635 self.returned = true
636 }
637
638 access(contract) fun setToSettled() {
639 self.settled = true
640 }
641
642 init (
643 flowtyStorefrontID: UInt64,
644 listingResourceID: UInt64,
645 paymentVaultType: Type,
646 nftType: Type,
647 nftID: UInt64,
648 term: UFix64
649 ) {
650 self.flowtyStorefrontID = flowtyStorefrontID
651 self.listingResourceID = listingResourceID
652 self.nftType = nftType
653 self.nftID = nftID
654 self.term = term
655 self.paymentVaultType = paymentVaultType
656
657 self.startTime = getCurrentBlock().timestamp
658 self.returned = false
659 self.settled = false
660 }
661 }
662
663 // RentalPublic
664 // An interface providing a useful public interface to a Rental.
665 //
666 access(all) resource interface RentalPublic {
667 // The entry point method to return a rental.
668 // The same NFT as the one that was rented must be returned.
669 access(all) fun returnNFT(nft: @{NonFungibleToken.NFT})
670
671 // Return the details of this Rental
672 access(all) view fun getDetails(): RentalDetails
673
674 // Return the listingDetails that were used to create this rental
675 access(all) view fun getListingDetails(): FlowtyRentals.ListingDetails
676
677 // How much time is left until this Rental has expired
678 access(all) view fun timeRemaining() : Fix64
679
680 // Whether this rental has expired and can be settled
681 access(all) view fun isRentalExpired(): Bool
682
683 access(all) fun settleRental()
684 }
685
686 // The resource used to represent a Rental.
687 // A Rental contains the deposit held in exchange for an NFT
688 // If that same NFT is returned, the deposit is released back to the renter.
689 access(all) resource Rental: RentalPublic, FlowtyListingCallback.Listing, Burner.Burnable {
690 access(all) event ResourceDestroyed(flowtyStorefrontID: UInt64 = self.details.flowtyStorefrontID, rentalResourceID: UInt64 = self.uuid, listingResourceID: UInt64 = self.details.listingResourceID)
691
692 // The non-resource data of a Rental
693 access(self) let details: RentalDetails
694
695 // The details of the listing that was rented
696 access(self) let listingDetails: ListingDetails
697
698 // Tokens held as a deposit
699 access(contract) var depositedFungibleTokens: @{FungibleToken.Vault}?
700
701 // reference to the original owner of the nft which was rented
702 access(contract) let ownerNFTCollectionPublic: Capability<&{NonFungibleToken.CollectionPublic}>
703 // reference to pay the original owner in the event that the rented NFT is not returned
704 access(contract) let ownerFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>
705
706 // Used to return the deposit held when the Rental was made
707 access(contract) let renterFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>
708
709 // Capability used to transfer the rented nft to our renter
710 access(contract) let renterNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>
711
712 // optional capability to automatically return the asset on settlement
713 access(contract) let renterNFTProvider: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.CollectionPublic, NonFungibleToken.Provider}>?
714
715 init (
716 // Rental details
717 storefrontID: UInt64,
718 listingResourceID: UInt64,
719 paymentVaultType: Type,
720 term: UFix64,
721 nftID: UInt64,
722 nftType: Type,
723 listingDetails: ListingDetails,
724
725 // Rental resource
726 ownerNFTCollectionPublic: Capability<&{NonFungibleToken.CollectionPublic}>,
727 renterFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
728 ownerFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
729 depositedFungibleTokens: @{FungibleToken.Vault}?,
730 renterNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
731 renterNFTProvider: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.CollectionPublic, NonFungibleToken.Provider}>?
732 ) {
733 self.ownerNFTCollectionPublic = ownerNFTCollectionPublic
734 self.renterFungibleTokenReceiver = renterFungibleTokenReceiver
735 self.ownerFungibleTokenReceiver = ownerFungibleTokenReceiver
736 self.depositedFungibleTokens <- depositedFungibleTokens
737 self.renterNFTProvider = renterNFTProvider
738
739 self.renterNFTCollection = renterNFTCollection
740 self.listingDetails = listingDetails
741
742 self.details = RentalDetails(
743 flowtyStorefrontID: storefrontID,
744 listingResourceID: listingResourceID,
745 paymentVaultType: paymentVaultType,
746 nftType: nftType,
747 nftID: nftID,
748 term: term
749 )
750 }
751
752 access(all) view fun getDetails(): RentalDetails {
753 return self.details
754 }
755
756 access(all) view fun getListingDetails(): ListingDetails {
757 return self.listingDetails
758 }
759
760 // The entry point method to return a rental.
761 // The same NFT as the one that was rented must be returned.
762 access(all) fun returnNFT(nft: @{NonFungibleToken.NFT}) {
763 pre {
764 !self.isRentalExpired(): "rental has expired"
765 !self.details.returned: "rental has been returned"
766 nft.getType() == self.details.nftType: "returned nft has the wrong type"
767 nft.id == self.details.nftID: "incorrect nftID"
768 }
769
770 // withdraw the deposited tokens when the rental was created
771 let deposit <- self.depositedFungibleTokens <- nil
772
773 self.details.setToReturned()
774
775 let depositor = FlowtyRentals.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
776
777 if let callback = FlowtyRentals.borrowCallbackContainer() {
778 callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: &nft as &{NonFungibleToken.NFT})
779 }
780
781 // get the fungibleToken vault capable of receiving the deposit so it can be returned
782 FlowtyUtils.trySendFungibleTokenVault(vault: <-deposit!, receiver: self.renterFungibleTokenReceiver, depositor: depositor)
783
784 // return the nft to the owner's collection
785 FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.ownerNFTCollectionPublic, depositor: depositor)
786
787 FlowtyRentals.borrowRoyaltiesLedger().remove(self.details.listingResourceID)
788
789 emit RentalReturned(
790 flowtyStorefrontAddress: self.ownerFungibleTokenReceiver.address,
791 flowtyStorefrontID: self.details.flowtyStorefrontID,
792 renterAddress: self.renterFungibleTokenReceiver.address,
793 listingResourceID: self.details.listingResourceID,
794 rentalResourceID: self.uuid,
795 nftID: self.details.nftID,
796 nftType: self.details.nftType.identifier
797 )
798 }
799
800 // settleRental can only be executed after the rental has expired.
801 // If the Renter supplied an optional provider which we can withdraw the NFT with,
802 // Then we will attempt to automatically take the NFT back. If this is possible, the deposit will
803 // be returned to the renter.
804 access(all) fun settleRental() {
805 pre {
806 self.isRentalExpired(): "rental hasn't expired"
807 self.details.returned == false: "rental has already been returned"
808 self.details.settled == false: "rental has already been settled"
809 }
810
811 let depositor = FlowtyRentals.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
812
813 // check if we can automatically return the NFT
814 // do we have a provider?
815 if self.renterNFTProvider != nil {
816 // borrow it.
817 if self.renterNFTProvider!.check() {
818 let renterNFTProvider = self.renterNFTProvider!.borrow()!
819 // does this NFT Collection have the ID that we need to withdraw?
820 let ids = renterNFTProvider.getIDs()
821 if ids.contains(self.details.nftID) {
822 let borrowedNFT = renterNFTProvider.borrowNFT(self.details.nftID)
823 if borrowedNFT != nil && borrowedNFT!.getType() == self.details.nftType && borrowedNFT!.id == self.details.nftID {
824 let nft <- renterNFTProvider.withdraw(withdrawID: self.details.nftID)
825 if nft.getType() == self.details.nftType && nft.id == self.details.nftID {
826
827 if let callback = FlowtyRentals.borrowCallbackContainer() {
828 callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: &nft as &{NonFungibleToken.NFT})
829 }
830
831 FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.ownerNFTCollectionPublic, depositor: depositor)
832
833 let deposit <- self.depositedFungibleTokens <- nil
834 FlowtyUtils.trySendFungibleTokenVault(vault: <-deposit!, receiver: self.renterFungibleTokenReceiver, depositor: depositor)
835
836 // it worked! the nft is returned
837 self.details.setToReturned()
838
839 FlowtyRentals.borrowRoyaltiesLedger().remove(self.details.listingResourceID)
840
841 emit RentalReturned(
842 flowtyStorefrontAddress: self.ownerFungibleTokenReceiver.address,
843 flowtyStorefrontID: self.details.flowtyStorefrontID,
844 renterAddress: self.renterFungibleTokenReceiver.address,
845 listingResourceID: self.details.listingResourceID,
846 rentalResourceID: self.uuid,
847 nftID: self.details.nftID,
848 nftType: self.details.nftType.identifier
849 )
850 return
851 } else {
852 // malicious actor has some weird NFT they're trying to send. we return it to them
853 // this path should only be able to be reached intentionally. At that point, we won't know if the
854 // receiver is setup properly to receive the borrowed item back or not so we should just make a lostandfound
855 // item for them and move on. If they can mess with a borrowed item returning a type that is different than
856 // the actual withdrawn nft, they can handle redeeming their item back from the lostandfound.
857
858 FlowtyUtils.depositToLostAndFound(
859 redeemer: self.renterNFTCollection.address,
860 item: <- nft,
861 memo: nil,
862 display: nil,
863 depositor: depositor
864 )
865 }
866 }
867 }
868 }
869 }
870
871 // we couldn't return the nft, settle it.
872 self.details.setToSettled()
873
874 if self.depositedFungibleTokens != nil {
875 // calculate the amounts to send to the owner and royalty
876 let vault <- self.depositedFungibleTokens <- nil
877 let amount = vault?.balance ?? panic("nil vault")
878 let royaltyFeeAmount = amount * self.listingDetails.royaltyRate
879
880 // get the vaults for payment
881 let v <- vault!
882
883 let tokenInfo = FlowtyUtils.getTokenInfo(self.details.paymentVaultType)!
884 let royaltyTokenPath = tokenInfo.receiverPath
885
886 let royalties = RoyaltiesLedger.get(self.details.listingResourceID)
887 if royalties != nil {
888 let tokenInfo = FlowtyUtils.getTokenInfo(self.details.paymentVaultType)!
889 let royaltyCuts = FlowtyUtils.metadataRoyaltiesToRoyaltyCuts(tokenInfo: tokenInfo, mdRoyalties: [royalties!])
890
891 let royaltyFeeCut <- v.withdraw(amount: royaltyFeeAmount)
892 if let leftover <- FlowtyUtils.distributeRoyaltiesWithDepositor(royaltyCuts: royaltyCuts, depositor: depositor, vault: <-royaltyFeeCut) {
893 v.deposit(from: <-leftover)
894 }
895 }
896
897 // distribute the rest to the original owner
898 FlowtyUtils.trySendFungibleTokenVault(vault: <-v, receiver: self.listingDetails.getPaymentCut().receiver, depositor: depositor)
899 }
900
901 FlowtyRentals.borrowRoyaltiesLedger().remove(self.details.listingResourceID)
902
903 if let callback = FlowtyRentals.borrowCallbackContainer() {
904 callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: nil)
905 }
906
907 emit RentalSettled(
908 rentalResourceID: self.uuid,
909 listingResourceID: self.details.listingResourceID,
910 renter: self.renterFungibleTokenReceiver.address,
911 lender: self.ownerFungibleTokenReceiver.address,
912 nftID: self.details.nftID,
913 nftType: self.details.nftType.identifier,
914 deposit: self.listingDetails.deposit
915 )
916 }
917
918 access(contract) fun burnCallback() {
919 pre {
920 self.details.settled || self.details.returned: "rental must be returned or settled to be destroyed"
921 self.depositedFungibleTokens == nil || self.depositedFungibleTokens?.balance == 0.0: "deposit balance is not 0"
922 }
923 }
924
925 // how much time is left to return this rental
926 access(all) view fun timeRemaining() : Fix64 {
927 let rentalTerm = self.details.term
928 let startTime = self.details.startTime
929 let currentTime = getCurrentBlock().timestamp
930 let remaining = Fix64(startTime + rentalTerm) - Fix64(currentTime)
931 return remaining
932 }
933
934 access(all) view fun isRentalExpired() : Bool {
935 return self.timeRemaining() < Fix64(0.0)
936 }
937 }
938
939 // FlowtyRentalsMarketplaceManager
940 // An interface for adding and removing rental resources within a FlowtyRentalsMarketplace,
941 //
942 access(all) resource interface FlowtyRentalsMarketplaceManager {
943 access(all) event ResourceDestroyed(id: UInt64 = self.uuid)
944
945 // createFunding
946 // Allows the FlowtyRentalsMarketplace owner to create and insert Fundings.
947 //
948 access(contract) fun createRental(
949 storefrontID: UInt64,
950 listingResourceID: UInt64,
951 nftID: UInt64,
952 nftType: Type,
953 paymentVaultType: Type,
954 term: UFix64,
955 listingDetails: ListingDetails,
956 ownerNFTCollectionPublic: Capability<&{NonFungibleToken.CollectionPublic}>,
957 ownerFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
958 renterFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
959 depositedFungibleTokens: @{FungibleToken.Vault}?,
960 renterNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
961 renterNFTProvider: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.CollectionPublic, NonFungibleToken.Provider}>?
962 ): &Rental
963 // removeRental
964 // Allows the FlowtyRentalsMarketplace owner to remove any rental resource.
965 //
966 access(Administrator) fun removeRental(rentalResourceID: UInt64)
967 }
968
969 access(all) resource interface FlowtyRentalsMarketplacePublic {
970 access(all) view fun getRentalIDs(): [UInt64]
971 access(all) view fun borrowRental(rentalResourceID: UInt64): &{RentalPublic}?
972 }
973
974 access(all) resource FlowtyRentalsMarketplace: FlowtyRentalsMarketplaceManager, FlowtyRentalsMarketplacePublic, Burner.Burnable {
975 access(all) event ResourceDestroyed(flowtyStorefrontID: UInt64 = self.uuid)
976
977 access(self) var rentals: @{UInt64: Rental}
978
979 // create a rental and store it on our contract marketplace
980 access(contract) fun createRental(
981 storefrontID: UInt64,
982 listingResourceID: UInt64,
983 nftID: UInt64,
984 nftType: Type,
985 paymentVaultType: Type,
986 term: UFix64,
987 listingDetails: ListingDetails,
988 ownerNFTCollectionPublic: Capability<&{NonFungibleToken.CollectionPublic}>,
989 ownerFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
990 renterFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
991 depositedFungibleTokens: @{FungibleToken.Vault}?,
992 renterNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
993 renterNFTProvider: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.CollectionPublic, NonFungibleToken.Provider}>?
994 ): &Rental {
995 let renter = renterFungibleTokenReceiver.address
996 let owner = ownerNFTCollectionPublic.address
997
998 // Create funding resource
999 let rental <- create Rental(
1000 storefrontID: storefrontID,
1001 listingResourceID: listingResourceID,
1002 paymentVaultType: paymentVaultType,
1003 term: term,
1004 nftID: nftID,
1005 nftType: nftType,
1006 listingDetails: listingDetails,
1007 ownerNFTCollectionPublic: ownerNFTCollectionPublic,
1008 renterFungibleTokenReceiver: renterFungibleTokenReceiver,
1009 ownerFungibleTokenReceiver: ownerFungibleTokenReceiver,
1010 depositedFungibleTokens: <-depositedFungibleTokens,
1011 renterNFTCollection: renterNFTCollection,
1012 renterNFTProvider: renterNFTProvider
1013 )
1014
1015 let rentalResourceID = rental.uuid
1016
1017 // set the rental item here so we can reference it later
1018 let oldRental <- self.rentals[rentalResourceID] <- rental
1019
1020 // Note that oldRental will always be nil, but we have to handle it.
1021 destroy oldRental
1022 return (&self.rentals[rentalResourceID])!
1023 }
1024
1025 // removes a rental from our contract
1026 access(Administrator) fun removeRental(rentalResourceID: UInt64) {
1027 let rental <- self.rentals.remove(key: rentalResourceID)
1028 ?? panic("missing Rental")
1029
1030 if let callback = FlowtyRentals.borrowCallbackContainer() {
1031 callback.handle(stage: FlowtyListingCallback.Stage.Destroyed, listing: &rental as &{FlowtyListingCallback.Listing}, nft: nil)
1032 }
1033
1034 assert(rental.getDetails().returned == true || rental.getDetails().settled == true, message: "rental is not returned or settled")
1035
1036 Burner.burn(<-rental)
1037 }
1038
1039 access(all) view fun getRentalIDs(): [UInt64] {
1040 return self.rentals.keys
1041 }
1042
1043 access(all) view fun borrowRental(rentalResourceID: UInt64): &{RentalPublic}? {
1044 if self.rentals[rentalResourceID] != nil {
1045 return &self.rentals[rentalResourceID]
1046 } else {
1047 return nil
1048 }
1049 }
1050
1051 access(contract) fun burnCallback() {
1052 let ids = self.rentals.keys
1053 for id in ids {
1054 Burner.burn(<- self.rentals.remove(key: id)!)
1055 }
1056 }
1057
1058 init () {
1059 self.rentals <- {}
1060
1061 // Let event consumers know that this storefront exists
1062 emit FlowtyRentalsMarketplaceInitialized(flowtyRentalsMarketplaceResourceID: self.uuid)
1063 }
1064 }
1065
1066 access(all) resource interface FlowtyRentalsStorefrontManager {
1067 access(List) fun createListing(
1068 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
1069 nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
1070 ownerFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
1071 nftType: Type,
1072 nftID: UInt64,
1073 amount: UFix64,
1074 deposit: UFix64,
1075 term: UFix64,
1076 paymentVaultType: Type,
1077 paymentCut: Flowty.PaymentCut,
1078 expiresAfter: UFix64,
1079 renter: Address?
1080 ): UInt64
1081
1082 access(Cancel) fun removeListing(listingResourceID: UInt64)
1083 }
1084
1085 access(all) resource interface FlowtyRentalsStorefrontPublic {
1086 access(all) view fun getListingIDs(): [UInt64]
1087 access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}?{
1088 post {
1089 result == nil || result!.getType() == Type<@Listing>()
1090 }
1091 }
1092 access(all) fun cleanup(listingResourceID: UInt64)
1093 }
1094
1095 // FlowtyRentalsStorefront - The storefront which stores listing and provides functionality to fill them
1096 // A listing records the an nftid and type, a fee and deposit, and an optional address.
1097 access(all) resource FlowtyRentalsStorefront : FlowtyRentalsStorefrontManager, FlowtyRentalsStorefrontPublic, Burner.Burnable {
1098 access(all) event ResourceDestroyed(flowtyStorefrontID: UInt64 = self.uuid)
1099
1100 access(self) var listings: @{UInt64: Listing}
1101
1102 // create a new listing. Takes in a provider to withdraw the listed nft, and details
1103 // about the terms of the rental and ways to send out payment
1104 access(List) fun createListing(
1105 nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
1106 nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
1107 ownerFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
1108 nftType: Type,
1109 nftID: UInt64,
1110 amount: UFix64,
1111 deposit: UFix64,
1112 term: UFix64,
1113 paymentVaultType: Type,
1114 paymentCut: Flowty.PaymentCut,
1115 expiresAfter: UFix64,
1116 renter: Address?
1117 ): UInt64 {
1118 pre {
1119 FlowtyUtils.isTokenSupported(type: paymentVaultType): "provided payment type is not supported"
1120 paymentCut.receiver.check() && paymentCut.receiver.borrow()!.getType() == paymentVaultType: "paymentCut receiver type and paymentVaultType do not match"
1121 nftProviderCapability.check(): "invalid nft provider"
1122 }
1123
1124 let ref = nftProviderCapability.borrow()!.borrowNFT(nftID) ?? panic("could not borrow nft")
1125 assert(FlowtyUtils.isSupported(ref), message: "nft type is not supported")
1126 let royalties = ref.resolveView(Type<MetadataViews.Royalties>()) as! MetadataViews.Royalties?
1127
1128 // create the listing
1129 let listing <- create Listing(
1130 nftProviderCapability: nftProviderCapability,
1131 nftPublicCollectionCapability: nftPublicCollectionCapability,
1132 ownerFungibleTokenReceiver: ownerFungibleTokenReceiver,
1133 nftType: nftType,
1134 nftID: nftID,
1135 amount: amount,
1136 deposit: deposit,
1137 term: term,
1138 paymentVaultType: paymentVaultType,
1139 paymentCut: paymentCut,
1140 storefrontID: self.uuid,
1141 expiresAfter: expiresAfter,
1142 renter: renter
1143 )
1144
1145 if royalties != nil {
1146 FlowtyRentals.borrowRoyaltiesLedger().set(listing.uuid, royalties!)
1147 }
1148
1149 let listingResourceID = listing.uuid
1150 let royaltyRate = listing.getDetails().royaltyRate
1151 let expiration = listing.getDetails().expiresAfter
1152
1153 // Add the new listing to the dictionary.
1154 let oldListing <- self.listings[listingResourceID] <- listing
1155 // Note that oldListing will always be nil, but we have to handle it.
1156 destroy oldListing
1157
1158 emit ListingAvailable(
1159 flowtyStorefrontAddress: self.owner!.address,
1160 flowtyStorefrontID: self.uuid,
1161 listingResourceID: listingResourceID,
1162 nftType: nftType.identifier,
1163 nftID: nftID,
1164 amount: amount,
1165 deposit: deposit,
1166 term: term,
1167 royaltyRate: royaltyRate,
1168 expiresAfter: expiration,
1169 paymentTokenType: paymentVaultType.identifier,
1170 renter: renter
1171 )
1172
1173 return listingResourceID
1174 }
1175
1176 access(Cancel) fun removeListing(listingResourceID: UInt64) {
1177 let listing <- self.listings.remove(key: listingResourceID)
1178 ?? panic("missing Listing")
1179 Burner.burn(<-listing)
1180 }
1181
1182 access(all) view fun getListingIDs(): [UInt64] {
1183 return self.listings.keys
1184 }
1185
1186 access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
1187 return &self.listings[listingResourceID]
1188 }
1189
1190 access(all) fun cleanup(listingResourceID: UInt64) {
1191 pre {
1192 self.listings[listingResourceID] != nil: "could not find listing with given id"
1193 }
1194
1195 let listing <- self.listings.remove(key: listingResourceID)!
1196 assert(listing.getDetails().rented == true, message: "listing is not rented, only admin can remove")
1197 Burner.burn(<-listing)
1198 }
1199
1200 access(contract) fun burnCallback() {
1201 for id in self.listings.keys {
1202 Burner.burn(<- self.listings.remove(key: id)!)
1203 }
1204
1205 // Let event consumers know that this storefront will no longer exist
1206 emit FlowtyRentalsStorefrontDestroyed(flowtyRentalsStorefrontResourceID: self.uuid)
1207 }
1208
1209 init () {
1210 self.listings <- {}
1211
1212 // Let event consumers know that this storefront exists
1213 emit FlowtyRentalsStorefrontInitialized(flowtyRentalsStorefrontResourceID: self.uuid)
1214 }
1215 }
1216
1217 // deprecated
1218 access(all) resource FlowtyAdmin {}
1219
1220 access(all) fun createStorefront(): @FlowtyRentalsStorefront {
1221 return <-create FlowtyRentalsStorefront()
1222 }
1223
1224 access(account) fun borrowMarketplace(): &FlowtyRentals.FlowtyRentalsMarketplace {
1225 return self.account.storage.borrow<&FlowtyRentals.FlowtyRentalsMarketplace>(from: FlowtyRentals.FlowtyRentalsMarketplaceStoragePath)!
1226 }
1227
1228 access(all) view fun borrowMarketplacePublic(): &{FlowtyRentalsMarketplacePublic} {
1229 let mp = self.account.storage.borrow<&{FlowtyRentalsMarketplacePublic}>(from: FlowtyRentals.FlowtyRentalsMarketplaceStoragePath)!
1230 return mp
1231 }
1232
1233 access(all) view fun borrowStorefrontPublic(addr: Address): &{FlowtyRentalsStorefrontPublic}? {
1234 return getAccount(addr).capabilities.get<&{FlowtyRentalsStorefrontPublic}>(FlowtyRentals.FlowtyRentalsStorefrontPublicPath).borrow()
1235 }
1236
1237 access(all) fun settleRental(rentalResourceID: UInt64){
1238 let marketplace = FlowtyRentals.borrowMarketplace()
1239 let rental = marketplace.borrowRental(rentalResourceID: rentalResourceID)
1240 rental!.settleRental()
1241 }
1242
1243 access(all) resource FlowtyRentalsAdmin {
1244 access(Administrator) fun setFees(rentalFee: UFix64) {
1245 pre {
1246 rentalFee <= 1.0: "Funding fee should be a percentage"
1247 }
1248
1249 FlowtyRentals.Fee = rentalFee
1250 }
1251
1252 access(Administrator) fun setSuspendedFundingPeriod(period: UFix64) {
1253 FlowtyRentals.SuspendedFundingPeriod = period
1254 }
1255 }
1256
1257 access(contract) fun borrowCallbackContainer(): auth(FlowtyListingCallback.Handle) &FlowtyListingCallback.Container? {
1258 return self.account.storage.borrow<auth(FlowtyListingCallback.Handle) &FlowtyListingCallback.Container>(from: FlowtyListingCallback.ContainerStoragePath)
1259 }
1260
1261 access(contract) fun borrowRoyaltiesLedger(): auth(RoyaltiesLedger.Administrator) &RoyaltiesLedger.Ledger {
1262 return self.account.storage.borrow<auth(RoyaltiesLedger.Administrator) &RoyaltiesLedger.Ledger>(from: RoyaltiesLedger.StoragePath)!
1263 }
1264
1265 init () {
1266 self.FlowtyRentalsStorefrontStoragePath = /storage/FlowtyRentalsStorefront
1267 self.FlowtyRentalsStorefrontPublicPath = /public/FlowtyRentalsStorefront
1268 self.FlowtyRentalsMarketplaceStoragePath = /storage/FlowtyRentalsMarketplace
1269 self.FlowtyRentalsMarketplacePublicPath = /public/FlowtyRentalsMarketplace
1270 self.FlowtyRentalsAdminStoragePath = /storage/FlowtyRentalsAdmin
1271
1272 self.Fee = 0.05 // Percentage of the rental amount taken as a fee
1273 self.SuspendedFundingPeriod = 1.0 // Period in seconds until the listing is valid
1274
1275 let marketplace <- create FlowtyRentalsMarketplace()
1276
1277 self.account.storage.save(<-marketplace, to: self.FlowtyRentalsMarketplaceStoragePath)
1278 let mpPubCap = self.account.capabilities.storage.issue<&{FlowtyRentals.FlowtyRentalsMarketplacePublic}>(FlowtyRentals.FlowtyRentalsMarketplaceStoragePath)
1279 self.account.capabilities.publish(mpPubCap, at: FlowtyRentals.FlowtyRentalsMarketplacePublicPath)
1280
1281 // FlowtyAdmin
1282 let flowtyAdmin <- create FlowtyRentalsAdmin()
1283 self.account.storage.save(<-flowtyAdmin, to: self.FlowtyRentalsAdminStoragePath)
1284
1285 emit FlowtyRentalsInitialized()
1286 }
1287}