Smart Contract

Flowty

A.5c57f79c6694797f.Flowty

Valid From

140,911,304

Deployed

2w ago
Feb 07, 2026, 04:07:44 AM UTC

Dependents

36 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import MetadataViews from 0x1d7e57aa55817448
4import LostAndFound from 0x473d6a2c37eab5be
5import Burner from 0xf233dcee88fe0abe
6
7import FlowtyUtils from 0x3cdbb3d569211ff3
8import FlowtyListingCallback from 0x3cdbb3d569211ff3
9import DNAHandler from 0x3cdbb3d569211ff3
10
11// Flowty
12//
13// A smart contract responsible for the main lending flows. 
14// It is facilitating the lending deal, allowing borrowers and lenders 
15// to be sure that the deal would be executed on their agreed terms.
16// 
17// Each account that wants to list a loan installs a Storefront,
18// and lists individual loan within that Storefront as Listings.
19// There is one Storefront per account, it handles loans of all NFT types
20// for that account.
21//
22// Each Listing can have one or more "cut"s of the requested loan amount that
23// goes to one or more addresses. Cuts are used to pay listing fees
24// or other considerations.
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 fund the listed loan.
30//
31access(all) contract Flowty {
32
33    // permits creating new listings on a storefront resource
34    access(all) entitlement List
35
36    // permits removing listings from a storefront resource
37    access(all) entitlement Cancel
38
39    access(all) entitlement Administrator
40
41    // FlowtyInitialized
42    // This contract has been deployed.
43    // Event consumers can now expect events from this contract.
44    //
45    access(all) event FlowtyInitialized()
46
47    // FlowtyStorefrontInitialized
48    // A FlowtyStorefront resource has been created.
49    // Event consumers can now expect events from this FlowtyStorefront.
50    // Note that we do not specify an address: we cannot and should not.
51    // Created resources do not have an owner address, and may be moved
52    // after creation in ways we cannot check.
53    // ListingAvailable events can be used to determine the address
54    // of the owner of the FlowtyStorefront (...its location) at the time of
55    // the listing but only at that precise moment in that precise transaction.
56    // If the seller moves the FlowtyStorefront while the listing is valid, 
57    // that is on them.
58    //
59    access(all) event FlowtyStorefrontInitialized(flowtyStorefrontResourceID: UInt64)
60
61    // FlowtyMarketplaceInitialized
62    // A FlowtyMarketplace resource has been created.
63    // Event consumers can now expect events from this FlowtyStorefront.
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    // ListingAvailable events can be used to determine the address
68    // of the owner of the FlowtyStorefront (...its location) at the time of
69    // the listing but only at that precise moment in that precise transaction.
70    // If the seller moves the FlowtyStorefront while the listing is valid, 
71    // that is on them.
72    //
73    access(all) event FlowtyMarketplaceInitialized(flowtyMarketplaceResourceID: UInt64)
74
75    // FlowtyStorefrontDestroyed
76    // A FlowtyStorefront has been destroyed.
77    // Event consumers can now stop processing events from this FlowtyStorefront.
78    // Note that we do not specify an address.
79    //
80    access(all) event FlowtyStorefrontDestroyed(flowtyStorefrontResourceID: UInt64)
81
82    // FlowtyMarketplaceDestroyed
83    // A FlowtyMarketplace has been destroyed.
84    // Event consumers can now stop processing events from this FlowtyMarketplace.
85    // Note that we do not specify an address.
86    //
87    access(all) event FlowtyMarketplaceDestroyed(flowtyStorefrontResourceID: UInt64)
88
89    // ListingAvailable
90    // A listing has been created and added to a FlowtyStorefront resource.
91    // The Address values here are valid when the event is emitted, but
92    // the state of the accounts they refer to may be changed outside of the
93    // FlowtyMarketplace workflow, so be careful to check when using them.
94    //
95    access(all) event ListingAvailable(
96        flowtyStorefrontAddress: Address,
97        flowtyStorefrontID: UInt64, 
98        listingResourceID: UInt64,
99        nftType: String,
100        nftID: UInt64,
101        amount: UFix64,
102        interestRate: UFix64,
103        term: UFix64,
104        enabledAutoRepayment: Bool,
105        royaltyRate: UFix64,
106        expiresAfter: UFix64,
107        paymentTokenType: String,
108        repaymentAddress: Address?
109    )
110
111    // ListingCompleted
112    // The listing has been resolved. It has either been funded, or removed and destroyed.
113    //
114    access(all) event ListingCompleted(
115        listingResourceID: UInt64, 
116        flowtyStorefrontID: UInt64, 
117        funded: Bool,
118        nftID: UInt64,
119        nftType: String,
120        flowtyStorefrontAddress: Address
121    )
122
123    // FundingAvailable
124    // A funding has been created and added to a FlowtyStorefront resource.
125    // The Address values here are valid when the event is emitted, but
126    // the state of the accounts they refer to may be changed outside of the
127    // FlowtyMarketplace workflow, so be careful to check when using them.
128    //
129    access(all) event FundingAvailable(
130        fundingResourceID: UInt64,
131        listingResourceID: UInt64,
132        borrower: Address,
133        lender: Address,
134        nftID: UInt64,
135        nftType: String,
136        repaymentAmount: UFix64,
137        enabledAutoRepayment: Bool,
138        repaymentAddress: Address?
139    )
140
141    // FundingRepaid
142    // A funding has been repaid.
143    //
144    access(all) event FundingRepaid(
145        fundingResourceID: UInt64,
146        listingResourceID: UInt64,
147        borrower: Address,
148        lender: Address,
149        nftID: UInt64,
150        nftType: String,
151        repaymentAmount: UFix64,
152        repaymentAddress: Address?
153    )
154
155    // FundingSettled
156    // A funding has been settled.
157    //
158    access(all) event FundingSettled(
159        fundingResourceID: UInt64,
160        listingResourceID: UInt64,
161        borrower: Address,
162        lender: Address,
163        nftID: UInt64,
164        nftType: String,
165        repaymentAmount: UFix64,
166        repaymentAddress: Address?
167    )
168
169    access(all) event CollectionSupportChanged(
170        collectionIdentifier: String,
171        state: Bool
172    )
173
174    access(all) event RoyaltyAdded(
175        collectionIdentifier: String,
176        rate: UFix64
177    )
178
179    access(all) event RoyaltyEscrow(
180        fundingResourceID: UInt64,
181        listingResourceID: UInt64,
182        lender: Address,
183        amount: UFix64
184    )
185
186    // FlowtyStorefrontStoragePath
187    // The location in storage that a FlowtyStorefront resource should be located.
188    access(all) let FlowtyStorefrontStoragePath: StoragePath
189
190    // FlowtyMarketplaceStoragePath
191    // The location in storage that a FlowtyMarketplace resource should be located.
192    access(all) let FlowtyMarketplaceStoragePath: StoragePath
193
194    // FlowtyStorefrontPublicPath
195    // The public location for a FlowtyStorefront link.
196    access(all) let FlowtyStorefrontPublicPath: PublicPath
197
198    // FlowtyMarketplacePublicPath
199    // The public location for a FlowtyMarketplace link.
200    access(all) let FlowtyMarketplacePublicPath: PublicPath
201
202    // FlowtyAdminStoragePath
203    // The location in storage that an FlowtyAdmin resource should be located.
204    access(all) let FlowtyAdminStoragePath: StoragePath
205
206    // FusdVaultStoragePath
207    // The location in storage that an FUSD Vault resource should be located.
208    access(all) let FusdVaultStoragePath: StoragePath
209
210    // FusdReceiverPublicPath
211    // The public location for a FUSD Receiver link.
212    access(all) let FusdReceiverPublicPath: PublicPath
213
214    // FusdBalancePublicPath
215    // The public location for a FUSD Balance link.
216    access(all) let FusdBalancePublicPath: PublicPath
217
218    // ListingFee
219    // The fixed fee in FUSD for a listing.
220    access(all) var ListingFee: UFix64
221
222    // FundingFee
223    // The percentage fee on funding, a number between 0 and 1.
224    access(all) var FundingFee: UFix64
225
226    // SuspendedFundingPeriod
227    // The suspended funding period in seconds(started on listing). 
228    // So that the borrower has some time to delist it.
229    access(all) var SuspendedFundingPeriod: UFix64
230
231    // A dictionary for the Collection to royalty configuration.
232    access(account) var Royalties: {String:Royalty}
233    access(account) var TokenPaths: {String:PublicPath}
234
235    // The collections which are allowed to be used as collateral
236    access(account) var SupportedCollections: {String:Bool}
237
238    // PaymentCut
239    // A struct representing a recipient that must be sent a certain amount
240    // of the payment when a tx is executed.
241    //
242    access(all) struct PaymentCut {
243        // The receiver for the payment.
244        // Note that we do not store an address to find the Vault that this represents,
245        // as the link or resource that we fetch in this way may be manipulated,
246        // so to find the address that a cut goes to you must get this struct and then
247        // call receiver.borrow().owner.address on it.
248        // This can be done efficiently in a script.
249        access(all) let receiver: Capability<&{FungibleToken.Receiver}>
250
251        // The amount of the payment FungibleToken that will be paid to the receiver.
252        access(all) let amount: UFix64
253
254        // initializer
255        //
256        init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
257            self.receiver = receiver
258            self.amount = amount
259        }
260    }
261
262    access(all) struct Royalty {
263        // The percentage points that should go to the collection owner
264        // In the event of a loan default
265        access(all) let Rate: UFix64
266        access(all) let Address: Address
267
268        init(rate: UFix64, address: Address) {
269            self.Rate = rate
270            self.Address = address
271        }
272    }
273
274    // ListingDetails
275    // A struct containing a Listing's data.
276    //
277    access(all) struct ListingDetails {
278        // The FlowtyStorefront that the Listing is stored in.
279        // Note that this resource cannot be moved to a different FlowtyStorefront
280        access(all) var flowtyStorefrontID: UInt64
281        // Whether this listing has been funded or not.
282        access(all) var funded: Bool
283        // The Type of the NonFungibleToken.NFT that is being listed.
284        access(all) let nftType: Type
285        // The ID of the NFT within that type.
286        access(all) let nftID: UInt64
287        // The amount of the requested loan.
288        access(all) let amount: UFix64
289        // The interest rate in %, a number between 0 and 1.
290        access(all) let interestRate: UFix64
291        //The term in seconds for this listing.
292        access(all) var term: UFix64
293        // The Type of the FungibleToken that fundings must be made in.
294        access(all) let paymentVaultType: Type
295        // This specifies the division of payment between recipients.
296        access(self) let paymentCuts: [PaymentCut]
297        //The time the funding start at
298        access(all) var listedTime: UFix64
299        // The royalty rate needed as a deposit for this loan to be funded
300        access(all) var royaltyRate: UFix64
301        // The number of seconds this listing is valid for
302        access(all) var expiresAfter: UFix64
303
304        // getPaymentCuts
305        // Returns payment cuts
306        access(all) view fun getPaymentCuts(): [PaymentCut] {
307            return self.paymentCuts
308        }
309
310        access(all) view fun getTotalPayment(): UFix64 {
311            return self.amount * (1.0 + (self.interestRate * Flowty.FundingFee) + self.royaltyRate)
312        }
313
314        // setToFunded
315        // Irreversibly set this listing as funded.
316        //
317        access(contract) fun setToFunded() {
318            self.funded = true
319        }
320
321        // initializer
322        //
323        init (
324            nftType: Type,
325            nftID: UInt64,
326            amount: UFix64,
327            interestRate: UFix64,
328            term: UFix64,
329            paymentVaultType: Type,
330            paymentCuts: [PaymentCut],
331            flowtyStorefrontID: UInt64,
332            expiresAfter: UFix64,
333            royaltyRate: UFix64
334        ) {
335            self.flowtyStorefrontID = flowtyStorefrontID
336            self.funded = false
337            self.nftType = nftType
338            self.nftID = nftID
339            self.amount = amount
340            self.interestRate = interestRate
341            self.term = term
342            self.paymentVaultType = paymentVaultType
343            self.listedTime = getCurrentBlock().timestamp
344            self.expiresAfter = expiresAfter
345            self.royaltyRate = royaltyRate
346
347            assert(paymentCuts.length > 0, message: "Listing must have at least one payment cut recipient")
348            self.paymentCuts = paymentCuts
349
350            // Calculate the total price from the cuts
351            var cutsAmount = 0.0
352            // Perform initial check on capabilities, and calculate payment price from cut amounts.
353            for cut in self.paymentCuts {
354                // make sure we can borrow the receiver
355                cut.receiver.borrow()!
356                // Add the cut amount to the total price
357                cutsAmount = cutsAmount + cut.amount
358            }
359
360            assert(cutsAmount > 0.0, message: "Listing must have non-zero requested amount")
361        }
362    }
363
364    // ListingPublic
365    // An interface providing a useful public interface to a Listing.
366    //
367    access(all) resource interface ListingPublic {
368        // borrowNFT
369        // This will assert in the same way as the NFT standard borrowNFT()
370        // if the NFT is absent, for example if it has been sold via another listing.
371        //
372        access(all) view fun borrowNFT(): &{NonFungibleToken.NFT}?
373
374        // fund
375        // Fund the listing.
376        // This pays the beneficiaries and returns the token to the buyer.
377        //
378        access(all) fun fund(payment: @{FungibleToken.Vault}, 
379            lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>, 
380            lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>)
381
382        // getDetails
383        //
384        access(all) view fun getDetails(): ListingDetails
385
386        // suspensionTimeRemaining
387        // 
388        access(all) view fun suspensionTimeRemaining() : Fix64
389
390        // remainingTimeToFund
391        //
392        access(all) view fun remainingTimeToFund(): Fix64
393
394        // isFundingEnabled
395        //
396        access(all) view fun isFundingEnabled(): Bool 
397    }
398
399
400    // Listing
401    // A resource that allows an NFT to be fund for an amount of a given FungibleToken,
402    // and for the proceeds of that payment to be split between several recipients.
403    // 
404    access(all) resource Listing: ListingPublic, FlowtyListingCallback.Listing, Burner.Burnable {
405        access(all) event ResourceDestroyed(
406            listingResourceID: UInt64 = self.uuid,
407            flowtyStorefrontID: UInt64 = self.details.flowtyStorefrontID,
408            funded: Bool = self.details.funded,
409            nftID: UInt64 = self.details.nftID,
410            nftType: String = self.details.nftType.identifier,
411            flowtyStorefrontAddress: Address = self.nftPublicCollectionCapability.address
412        )
413
414        // The simple (non-Capability, non-complex) details of the listing
415        access(self) let details: ListingDetails
416
417        // A capability allowing this resource to withdraw the NFT with the given ID from its collection.
418        // This capability allows the resource to withdraw *any* NFT, so you should be careful when giving
419        // such a capability to a resource and always check its code to make sure it will use it in the
420        // way that it claims.
421        access(contract) let nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>
422
423        // A capability allowing this resource to access the owner's NFT public collection 
424        access(contract) let nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>
425
426        // A capability allowing this resource to withdraw `FungibleToken`s from borrower account.
427        // This capability allows loan repayment if there is system downtime, which will prevent NFT losing.
428        // NOTE: This variable cannot be renamed but it can allow any FungibleToken.
429        access(contract) let fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?
430
431        // borrowNFT
432        // This will assert in the same way as the NFT standard borrowNFT()
433        // if the NFT is absent, for example if it has been sold via another listing.
434        //
435        access(all) view fun borrowNFT(): &{NonFungibleToken.NFT}? {
436            if let cap = self.nftProviderCapability.borrow() {
437                if let ref = cap.borrowNFT(self.getDetails().nftID) {
438                    if ref.getType() != self.details.nftType || ref.id != self.details.nftID {
439                        return nil
440                    }
441                    return ref
442                }
443            }
444
445            return nil
446        }
447
448        // getDetails
449        // Get the details of the current state of the Listing as a struct.
450        // This avoids having more public variables and getter methods for them, and plays
451        // nicely with scripts (which cannot return resources).
452        //
453        access(all) view fun getDetails(): ListingDetails {
454            return self.details
455        }
456
457        // fund
458        // Fund the listing.
459        // This pays the beneficiaries and move the NFT to the funding resource stored in the marketplace account.
460        //
461        access(all) fun fund(payment: @{FungibleToken.Vault},
462            lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
463            lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>) {
464            pre {
465                self.isFundingEnabled(): "Funding is not enabled or this listing has expired"
466                self.details.funded == false: "listing has already been funded"
467                payment.isInstance(self.details.paymentVaultType): "payment vault is not requested fungible token"
468                payment.balance == self.details.getTotalPayment(): "payment vault does not contain requested amount"
469                self.nftProviderCapability.check(): "nftProviderCapability failed check"
470                self.details.term >= 1_209_600.0: "term must be at least 2 weeks"
471            }
472
473            // Make sure the listing cannot be funded again.
474            self.details.setToFunded()
475
476            // Fetch the token to return to the purchaser.
477            let nft <-self.nftProviderCapability.borrow()!.withdraw(withdrawID: self.details.nftID)
478            let ref = &nft as &{NonFungibleToken.NFT}
479            assert(FlowtyUtils.isSupported(ref), message: "nft type is not supported")
480
481            // Neither receivers nor providers are trustworthy, they must implement the correct
482            // interface but beyond complying with its pre/post conditions they are not gauranteed
483            // to implement the functionality behind the interface in any given way.
484            // Therefore we cannot trust the Collection resource behind the interface,
485            // and we must check the NFT resource it gives us to make sure that it is the correct one.
486            assert(nft.isInstance(self.details.nftType), message: "withdrawn NFT is not of specified type")
487            assert(nft.id == self.details.nftID, message: "withdrawn NFT does not have specified ID")
488
489            // Rather than aborting the transaction if any receiver is absent when we try to pay it,
490            // we send the cut to the first valid receiver.
491            // The first receiver should therefore either be the borrower, or an agreed recipient for
492            // any unpaid cuts.
493            var residualReceiver: &{FungibleToken.Receiver}? = nil
494
495            // Pay each beneficiary their amount of the payment.
496            for cut in self.details.getPaymentCuts() {
497                if cut.receiver.check() {
498                    let receiver = cut.receiver.borrow()!
499                    let paymentCut <- payment.withdraw(amount: cut.amount)
500                    receiver.deposit(from: <-paymentCut)
501                    if (residualReceiver == nil) {
502                        residualReceiver = receiver
503                    }
504                }
505            }
506
507            // Funding fee
508            let fundingFeeAmount = self.details.amount * self.details.interestRate * Flowty.FundingFee
509            let fundingFee <- payment.withdraw(amount: fundingFeeAmount)
510            let tokenInfo = FlowtyUtils.getTokenInfo(self.details.paymentVaultType)!
511
512            let flowtyFeeReceiver = Flowty.account.capabilities.get<&{FungibleToken.Receiver}>(tokenInfo.receiverPath)!.borrow()!
513            flowtyFeeReceiver.deposit(from: <-fundingFee)
514
515            // Royalty
516            // Deposit royalty amount 
517            let royalty = self.details.royaltyRate
518
519            var royaltyVault: @{FungibleToken.Vault}? <- nil
520            if self.details.royaltyRate > 0.0 {
521                let tmp <- royaltyVault <- payment.withdraw(amount: self.details.amount * royalty)
522                destroy tmp
523            }
524
525            assert(residualReceiver != nil, message: "No valid payment receivers")
526
527            // At this point, if all recievers were active and availabile, then the payment Vault will have
528            // zero tokens left, and this will functionally be a no-op that consumes the empty vault
529            residualReceiver!.deposit(from: <-payment)
530
531            let listingResourceID = self.uuid
532
533            // If the listing is funded, we regard it as completed here.
534            // Otherwise we regard it as completed in the destructor.
535            emit ListingCompleted(
536                listingResourceID: listingResourceID,
537                flowtyStorefrontID: self.details.flowtyStorefrontID,
538                funded: self.details.funded,
539                nftID: self.details.nftID,
540                nftType: self.details.nftType.identifier,
541                flowtyStorefrontAddress: self.nftPublicCollectionCapability.address
542            )
543
544            let repaymentAmount = self.details.amount + self.details.amount * self.details.interestRate
545
546            if let callback = Flowty.borrowCallbackContainer() {
547                callback.handle(stage: FlowtyListingCallback.Stage.Filled, listing: &self as &{FlowtyListingCallback.Listing}, nft: ref)
548            }
549
550            let marketplace = Flowty.borrowMarketplace()
551            marketplace.createFunding(
552                flowtyStorefrontID: self.details.flowtyStorefrontID, 
553                listingResourceID: listingResourceID, 
554                ownerNFTCollection: self.nftPublicCollectionCapability, 
555                lenderNFTCollection: lenderNFTCollection, 
556                NFT: <-nft, 
557                paymentVaultType: self.details.paymentVaultType,
558                lenderFungibleTokenReceiver: lenderFungibleTokenReceiver,
559                repaymentAmount: repaymentAmount,
560                term: self.details.term,
561                fusdProviderCapability: self.fusdProviderCapability,
562                royaltyVault: <-royaltyVault,
563                listingDetails: self.getDetails()
564            )
565        }
566
567        // suspensionTimeRemaining
568        // The remaining time. This can be negative if is expired
569        access(all) view fun suspensionTimeRemaining() : Fix64 {
570            let listedTime = self.details.listedTime
571            let currentTime = getCurrentBlock().timestamp
572
573            let remaining = Fix64(listedTime+Flowty.SuspendedFundingPeriod) - Fix64(currentTime)
574
575            return remaining
576        }
577
578        // remainingTimeToFund
579        // The time in seconds left until this listing is no longer valid
580        access(all) view fun remainingTimeToFund(): Fix64 {
581            let listedTime = self.details.listedTime
582            let currentTime = getCurrentBlock().timestamp
583            let remaining = Fix64(listedTime + self.details.expiresAfter) - Fix64(currentTime)
584            return remaining
585        }
586
587        // isFundingEnabled
588        access(all) view fun isFundingEnabled(): Bool {           
589            let timeRemaining = self.suspensionTimeRemaining()
590            let listingTimeRemaining = self.remainingTimeToFund()
591            return timeRemaining < Fix64(0.0) && listingTimeRemaining > Fix64(0.0)
592        }
593
594        // initializer
595        //
596        init (
597            nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
598            nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
599            fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
600            nftType: Type,
601            nftID: UInt64,
602            amount: UFix64,
603            interestRate: UFix64,
604            term: UFix64,
605            paymentVaultType: Type,
606            paymentCuts: [PaymentCut],
607            flowtyStorefrontID: UInt64,
608            expiresAfter: UFix64,
609            royaltyRate: UFix64
610        ) {
611            // Store the sale information
612            self.details = ListingDetails(
613                nftType: nftType,
614                nftID: nftID,
615                amount: amount,
616                interestRate: interestRate,
617                term: term,
618                paymentVaultType: paymentVaultType,
619                paymentCuts: paymentCuts,
620                flowtyStorefrontID: flowtyStorefrontID,
621                expiresAfter: expiresAfter,
622                royaltyRate: royaltyRate
623            )
624
625            // Store the NFT provider
626            self.nftProviderCapability = nftProviderCapability
627
628            self.fusdProviderCapability = fusdProviderCapability
629
630            self.nftPublicCollectionCapability = nftPublicCollectionCapability
631
632            // Check that the provider contains the NFT.
633            // We will check it again when the token is funded.
634            // We cannot move this into a function because initializers cannot call member functions.
635            let provider = self.nftProviderCapability.borrow()!
636
637            // This will precondition assert if the token is not available.
638            let nft = provider.borrowNFT(self.details.nftID) ?? panic("nft not found in provider capability")
639            assert(nft.isInstance(self.details.nftType), message: "token is not of specified type")
640            assert(nft.id == self.details.nftID, message: "token does not have specified ID")
641        }
642
643        access(contract) fun burnCallback() {
644            // We regard the listing as completed here.
645            emit ListingCompleted(
646                listingResourceID: self.uuid,
647                flowtyStorefrontID: self.details.flowtyStorefrontID,
648                funded: self.details.funded,
649                nftID: self.details.nftID,
650                nftType: self.details.nftType.identifier,
651                flowtyStorefrontAddress: self.nftPublicCollectionCapability.address
652            )
653        }
654    }
655
656    // FundingDetails
657    // A struct containing a Fundings's data.
658    //
659    access(all) struct FundingDetails {
660        // The FlowtyStorefront that the Funding is stored in.
661        // Note that this resource cannot be moved to a different FlowtyStorefront
662        access(all) var flowtyStorefrontID: UInt64
663        access(all) var listingResourceID: UInt64
664
665        // Whether this funding has been repaid or not.
666        access(all) var repaid: Bool
667
668        // Whether this funding has been settled or not.
669        access(all) var settled: Bool
670
671        // The Type of the FungibleToken that fundings must be repaid.
672        access(all) let paymentVaultType: Type
673
674        // The amount that must be repaid in the specified FungibleToken.
675        access(all) let repaymentAmount: UFix64
676
677        // the time the funding start at
678        access(all) var startTime: UFix64
679
680        // The length in seconds for this funding
681        access(all) var term: UFix64
682
683        // setToRepaid
684        // Irreversibly set this funding as repaid.
685        //
686        access(contract) fun setToRepaid() {
687            self.repaid = true
688        }
689
690        // setToSettled
691        // Irreversibly set this funding as settled.
692        //
693        access(contract) fun setToSettled() {
694            self.settled = true
695        }
696
697        // initializer
698        //
699        init (
700            flowtyStorefrontID: UInt64,
701            listingResourceID: UInt64,
702            paymentVaultType: Type,
703            repaymentAmount: UFix64, 
704            term: UFix64
705        ) {
706            self.flowtyStorefrontID = flowtyStorefrontID
707            self.listingResourceID = listingResourceID
708            self.paymentVaultType = paymentVaultType
709            self.repaid = false
710            self.settled = false
711            self.repaymentAmount = repaymentAmount
712            self.term = term
713            self.startTime = getCurrentBlock().timestamp
714        }
715    }
716
717    // FundingPublic
718    // An interface providing a useful public interface to a Funding.
719    //
720    access(all) resource interface FundingPublic {
721
722        // repay
723        //
724        access(all) fun repay(payment: @{FungibleToken.Vault})
725
726        // getDetails
727        //
728        access(all) view fun getDetails(): FundingDetails
729
730        // get the listing details for this loan
731        //
732        access(all) view fun getListingDetails(): Flowty.ListingDetails
733
734        // timeRemaining
735        // 
736        access(all) view fun timeRemaining() : Fix64
737
738        // isFundingExpired
739        //
740        access(all) view fun isFundingExpired(): Bool 
741
742        // get the amount stored in a vault for royalty payouts
743        //
744        access(all) view fun getRoyaltyAmount(): UFix64?
745
746        access(all) fun settleFunding()
747    }
748
749    // Funding
750    // 
751    access(all) resource Funding: FundingPublic, FlowtyListingCallback.Listing, Burner.Burnable {
752        access(all) event ResourceDestroyed(flowtyStorefrontID: UInt64 = self.details.flowtyStorefrontID, fundingResourceID: UInt64 = self.uuid, listingResourceID: UInt64 = self.details.listingResourceID)
753
754        // The simple (non-Capability, non-complex) details of the listing
755        access(self) let details: FundingDetails
756        access(self) let listingDetails: ListingDetails
757
758        // A capability allowing this resource to access the owner's NFT public collection 
759        access(contract) let ownerNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>
760
761        // A capability allowing this resource to access the lender's NFT public collection 
762        access(contract) let lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>
763
764        // The receiver for the repayment.
765        access(contract) let lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>
766
767        // NFT escrow
768        access(contract) var NFT: @{NonFungibleToken.NFT}?
769
770        // FUSD Allowance
771        access(contract) let fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?
772
773        // royalty payment vault to be deposited to the specified desination on repayment or default
774        access(contract) var royaltyVault: @{FungibleToken.Vault}?
775
776        // getDetails
777        // Get the details of the current state of the Listing as a struct.
778        // This avoids having more public variables and getter methods for them, and plays
779        // nicely with scripts (which cannot return resources).
780        //
781        access(all) view fun getDetails(): FundingDetails {
782            return self.details
783        }
784
785        access(all) view fun getListingDetails(): ListingDetails {
786            return self.listingDetails
787        }
788
789        access(all) view fun getRoyaltyAmount(): UFix64? {
790            return self.royaltyVault?.balance
791        }
792
793        access(contract) fun borrowNFT(): &{NonFungibleToken.NFT}? {
794            return &self.NFT
795        }
796
797        // repay
798        // Repay the funding.
799        // This pays the lender and returns the NFT to the owner.
800        //
801        access(all) fun repay(payment: @{FungibleToken.Vault}) {
802            pre {
803                self.details.repaid == false: "funding has already been repaid"
804                payment.isInstance(self.details.paymentVaultType): "payment vault is not requested fungible token"
805                payment.balance == self.details.repaymentAmount: "payment vault does not contain requested price"
806            }
807
808            self.details.setToRepaid()
809            let royaltyAmount = self.royaltyVault != nil ? self.royaltyVault?.balance! : 0.0
810
811            let tmp <- self.NFT <- nil
812            let nft <- tmp!
813            let nftID: UInt64 = nft.id
814            let nftType = nft.getType()
815
816            let depositor = Flowty.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
817            let royaltyVault <- self.royaltyVault <- nil
818            if royaltyVault != nil {
819                let vault <-! royaltyVault!
820                vault.deposit(from: <-payment.withdraw(amount: self.details.repaymentAmount))
821                destroy payment
822                assert(vault.balance == self.details.repaymentAmount + royaltyAmount, message: "insufficient balance to send to lender" )
823
824                FlowtyUtils.trySendFungibleTokenVault(vault: <-vault, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
825            } else {
826                FlowtyUtils.trySendFungibleTokenVault(vault: <-payment, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
827                destroy royaltyVault
828            }
829
830            if let callback = Flowty.borrowCallbackContainer() {
831                callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: nil)
832            }
833
834            FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.ownerNFTCollection, depositor: depositor)
835
836            let borrower = self.ownerNFTCollection.address
837            let lender = self.lenderFungibleTokenReceiver.address
838            emit FundingRepaid(
839                fundingResourceID: self.uuid, 
840                listingResourceID: self.details.listingResourceID, 
841                borrower: borrower,
842                lender: lender,
843                nftID: nftID,
844                nftType: nftType.identifier,
845                repaymentAmount: self.details.repaymentAmount,
846                repaymentAddress: self.fusdProviderCapability?.address
847            )
848        }
849
850        // repay
851        // Repay the funding with borrower permit.
852        // This pays the lender and returns the NFT to the owner using FUSD allowance from borrower account.
853        //
854        access(all) fun repayWithPermit() {
855            pre {
856                self.details.repaid == false: "funding has already been repaid"
857                self.details.settled == false: "funding has already been settled"
858                self.fusdProviderCapability!.check(): "listing is created without FUSD allowance"
859            }
860
861            assert(false, message: "repayWithPermit is disabled temporarily, for more info, please visit: https://x.com/flowty_io")
862
863            self.details.setToRepaid()
864            let royaltyAmount = self.royaltyVault != nil ? self.royaltyVault?.balance! : 0.0
865
866            let tmp <- self.NFT <- nil
867            let nft <- tmp!
868            let nftID = nft.id
869            let nftType = nft.getType()
870
871            let borrowerVault = self.fusdProviderCapability!.borrow()!
872            let payment <- borrowerVault.withdraw(amount: self.details.repaymentAmount)
873
874            if let callback = Flowty.borrowCallbackContainer() {
875                callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: nil)
876            }
877
878            let depositor = Flowty.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
879            FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.ownerNFTCollection, depositor: depositor)
880
881            let royaltyVault <- self.royaltyVault <- nil
882            let vault <-! royaltyVault!
883            vault.deposit(from: <-payment.withdraw(amount: self.details.repaymentAmount))
884            destroy payment
885            assert(vault.balance == self.details.repaymentAmount + royaltyAmount, message: "insufficient balance to send to lender" )
886
887            FlowtyUtils.trySendFungibleTokenVault(vault: <-vault, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
888
889            let borrower = self.ownerNFTCollection.address
890            let lender = self.lenderFungibleTokenReceiver.address
891            emit FundingRepaid(
892                fundingResourceID: self.uuid, 
893                listingResourceID: self.details.listingResourceID, 
894                borrower: borrower,
895                lender: lender,
896                nftID: nftID,
897                nftType: nftType.identifier,
898                repaymentAmount: self.details.repaymentAmount,
899                repaymentAddress: self.fusdProviderCapability?.address
900            )
901        }
902
903        // settleFunding
904        // Settle the different statuses responsible for the repayment and claiming processes.
905        // NFT is moved to the lender, because the borrower hasn't repaid the loan.
906        //
907        access(all) fun settleFunding() {
908            pre {
909                self.isFundingExpired(): "the loan hasn't expired"
910                !self.details.repaid: "funding has already been repaid"
911                !self.details.settled: "funding has already been settled"
912            }
913
914            assert(false, message: "loan settlement is disabled temporarily, for more info, please visit: https://x.com/flowty_io")
915
916            let lender = self.lenderNFTCollection.address
917            let borrower = self.ownerNFTCollection.address
918
919            let repayer = self.fusdProviderCapability?.address
920            let borrowerTokenBalance = repayer != nil ? FlowtyUtils.getTokenBalance(address: repayer!, vaultType: self.details.paymentVaultType) : 0.0
921
922            let ref = &self.NFT as &{NonFungibleToken.NFT}?
923            let royalties = ref!.resolveView(Type<MetadataViews.Royalties>()) as! MetadataViews.Royalties?
924
925            let tmp <- self.NFT <- nil
926            let nft <- tmp!
927            let nftID = nft.id
928            let nftType = nft.getType()
929
930            if let callback = Flowty.borrowCallbackContainer() {
931                callback.handle(stage: FlowtyListingCallback.Stage.Completed, listing: &self as &{FlowtyListingCallback.Listing}, nft: nil)
932            }
933
934            let depositor = Flowty.account.storage.borrow<auth(LostAndFound.Deposit) &LostAndFound.Depositor>(from: LostAndFound.DepositorStoragePath)!
935            if borrowerTokenBalance >= self.details.repaymentAmount && self.fusdProviderCapability?.check() == true {
936                // borrower has funds to repay loan
937                // repay lender
938                // return NFT to owner
939                self.details.setToRepaid()
940
941                let borrowerVault = self.fusdProviderCapability!.borrow()!
942                let payment <- borrowerVault.withdraw(amount: self.details.repaymentAmount)
943
944                FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.ownerNFTCollection, depositor: depositor)
945
946                let repaymentVault <- payment
947                let royaltyVault <- self.royaltyVault <- nil
948                if royaltyVault != nil {
949                    repaymentVault.deposit(from: <-royaltyVault!)
950                } else {
951                    destroy royaltyVault
952                }
953
954                FlowtyUtils.trySendFungibleTokenVault(vault: <-repaymentVault, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
955
956                emit FundingRepaid(
957                    fundingResourceID: self.uuid, 
958                    listingResourceID: self.details.listingResourceID, 
959                    borrower: borrower,
960                    lender: lender,
961                    nftID: nftID,
962                    nftType: nftType.identifier,
963                    repaymentAmount: self.details.repaymentAmount,
964                    repaymentAddress: self.fusdProviderCapability?.address
965                )
966
967                return
968            }
969            
970            // loan defaults; move NFT to lender as payment
971            self.details.setToSettled()
972            assert(nft != nil, message: "NFT is already moved")
973            FlowtyUtils.trySendNFT(nft: <-nft, receiver: self.lenderNFTCollection, depositor: depositor)
974        
975            emit FundingSettled(
976                fundingResourceID: self.uuid, 
977                listingResourceID: self.details.listingResourceID, 
978                borrower: borrower,
979                lender: lender,
980                nftID: nftID,
981                nftType: nftType.identifier,
982                repaymentAmount: self.details.repaymentAmount,
983                repaymentAddress: self.fusdProviderCapability?.address
984            )
985            
986            let royaltyVault <- self.royaltyVault <- nil
987            if royaltyVault == nil {
988                destroy royaltyVault
989                return
990            }
991
992            let v <- royaltyVault!
993            let originalBalance = v.balance
994            if v.balance == 0.0 {
995                destroy v
996                return
997            }
998
999            if royalties == nil {
1000                // no defined royalties on this NFT, return is back to the lender
1001                FlowtyUtils.trySendFungibleTokenVault(vault: <-v, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
1002                return
1003            }
1004
1005            // distribute royalties!
1006            let tokenInfo = FlowtyUtils.getTokenInfo(self.details.paymentVaultType)!
1007            let royaltyCuts = FlowtyUtils.metadataRoyaltiesToRoyaltyCuts(tokenInfo: tokenInfo, mdRoyalties: [royalties!])
1008            if let leftover <- FlowtyUtils.distributeRoyaltiesWithDepositor(royaltyCuts: royaltyCuts, depositor: depositor, vault: <-v) {
1009                FlowtyUtils.trySendFungibleTokenVault(vault: <-leftover, receiver: self.lenderFungibleTokenReceiver, depositor: depositor)
1010            }
1011        }
1012
1013        // timeRemaining
1014        // The remaining time. This can be negative if is expired
1015        access(all) view fun timeRemaining() : Fix64 {
1016            let fundingTerm = self.details.term
1017
1018            let startTime = self.details.startTime
1019            let currentTime = getCurrentBlock().timestamp
1020
1021            let remaining = Fix64(startTime+fundingTerm) - Fix64(currentTime)
1022
1023            return remaining
1024        }
1025
1026        // isFundingExpired
1027        access(all) view fun isFundingExpired(): Bool {
1028            let timeRemaining= self.timeRemaining()
1029            return timeRemaining < Fix64(0.0)
1030        }
1031
1032        // initializer
1033        //
1034        init (
1035            flowtyStorefrontID: UInt64,
1036            listingResourceID: UInt64,
1037            ownerNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1038            lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1039            NFT: @{NonFungibleToken.NFT},
1040            paymentVaultType: Type,
1041            repaymentAmount: UFix64,
1042            lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
1043            term: UFix64,
1044            fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1045            royaltyVault: @{FungibleToken.Vault}?,
1046            listingDetails: ListingDetails
1047        ) {
1048            self.ownerNFTCollection = ownerNFTCollection
1049            self.lenderNFTCollection = lenderNFTCollection
1050            self.lenderFungibleTokenReceiver = lenderFungibleTokenReceiver
1051            self.fusdProviderCapability = fusdProviderCapability
1052            self.listingDetails = listingDetails
1053            self.NFT <-NFT
1054            self.royaltyVault <-royaltyVault 
1055
1056            // Store the detailed information
1057            self.details = FundingDetails(
1058                flowtyStorefrontID: flowtyStorefrontID,
1059                listingResourceID: listingResourceID,
1060                paymentVaultType: paymentVaultType,
1061                repaymentAmount: repaymentAmount,
1062                term: term
1063            )
1064        }
1065
1066        access(contract) fun burnCallback() {
1067            pre {
1068                self.NFT == nil: "nft is not nil"
1069                self.royaltyVault == nil: "royalty vault if not nil"
1070            }
1071        }
1072    }
1073
1074    // FlowtyMarketplaceManager
1075    // An interface for adding and removing Fundings within a FlowtyMarketplace,
1076    // intended for use by the FlowtyStorefront's own
1077    //
1078    access(all) resource interface FlowtyMarketplaceManager {
1079        // createFunding
1080        // Allows the FlowtyMarketplace owner to create and insert Fundings.
1081        //
1082        access(contract) fun createFunding(
1083            flowtyStorefrontID: UInt64, 
1084            listingResourceID: UInt64,
1085            ownerNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1086            lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1087            NFT: @{NonFungibleToken.NFT},
1088            paymentVaultType: Type,
1089            lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
1090            repaymentAmount: UFix64,
1091            term: UFix64,
1092            fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1093            royaltyVault: @{FungibleToken.Vault}?,
1094            listingDetails: ListingDetails
1095        ): UInt64
1096        // removeFunding
1097        // Allows the FlowtyMarketplace owner to remove any funding.
1098        //
1099        access(Administrator) fun removeFunding(fundingResourceID: UInt64)
1100
1101        access(Administrator) view fun borrowPrivateFunding(fundingResourceID: UInt64): &Funding?
1102    }
1103
1104    // FlowtyMarketplacePublic
1105    // An interface to allow listing and borrowing Listings, and funding loans via Listings
1106    // in a FlowtyStorefront.
1107    //
1108    access(all) resource interface FlowtyMarketplacePublic {
1109        access(all) view fun getFundingIDs(): [UInt64]
1110        access(all) view fun borrowFunding(fundingResourceID: UInt64): &{FundingPublic}?
1111    }
1112
1113    // FlowtyStorefront
1114    // A resource that allows its owner to manage a list of Listings, and anyone to interact with them
1115    // in order to query their details and fund the loans that they represent.
1116    //
1117    access(all) resource FlowtyMarketplace : FlowtyMarketplaceManager, FlowtyMarketplacePublic, Burner.Burnable {
1118        // The dictionary of Fundings uuids to Funding resources.
1119        access(self) var fundings: @{UInt64: Funding}
1120
1121        // insert
1122        // Create and publish a funding for an NFT.
1123        //
1124        access(contract) fun createFunding(
1125            flowtyStorefrontID: UInt64, 
1126            listingResourceID: UInt64,
1127            ownerNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1128            lenderNFTCollection: Capability<&{NonFungibleToken.CollectionPublic}>,
1129            NFT: @{NonFungibleToken.NFT},
1130            paymentVaultType: Type,
1131            lenderFungibleTokenReceiver: Capability<&{FungibleToken.Receiver}>,
1132            repaymentAmount: UFix64,
1133            term: UFix64,
1134            fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1135            royaltyVault: @{FungibleToken.Vault}?,
1136            listingDetails: ListingDetails
1137         ): UInt64 {
1138            // FundingAvailable event fields
1139            let nftID = NFT.id
1140            let nftType = NFT.getType()
1141
1142            let lenderVaultCap = lenderFungibleTokenReceiver.borrow()!
1143            let lender = lenderVaultCap.owner!.address
1144
1145            let borrowerNFTCollectionCap = ownerNFTCollection.borrow()!
1146            let borrower = borrowerNFTCollectionCap.owner!.address
1147
1148            // Create funding resource
1149            let funding <- create Funding(
1150                flowtyStorefrontID: flowtyStorefrontID,
1151                listingResourceID: listingResourceID,
1152                ownerNFTCollection: ownerNFTCollection,
1153                lenderNFTCollection: lenderNFTCollection,
1154                NFT: <-NFT,
1155                paymentVaultType: paymentVaultType,
1156                repaymentAmount: repaymentAmount,
1157                lenderFungibleTokenReceiver: lenderFungibleTokenReceiver,
1158                term: term,
1159                fusdProviderCapability: fusdProviderCapability,
1160                royaltyVault: <-royaltyVault,
1161                listingDetails: listingDetails
1162            )
1163
1164            let detail = funding.getDetails()
1165            let expiration = detail.term + detail.startTime
1166            // September 3rd 12:00 AM
1167            assert(paymentVaultType.identifier != "A.b19436aae4d94622.FiatToken.Vault" || expiration < 1725321600.0, message: "loans are not able to be funded that expire on or after September 3rd 12:00 AM GMT.")
1168
1169            if let callback = Flowty.borrowCallbackContainer() {
1170                callback.handle(stage: FlowtyListingCallback.Stage.Created, listing: &funding as &{FlowtyListingCallback.Listing}, nft: funding.borrowNFT())
1171            }
1172
1173            let fundingResourceID = funding.uuid
1174
1175            // Add the new Funding to the dictionary.
1176            let oldFunding <- self.fundings[fundingResourceID] <- funding
1177            // Note that oldFunding will always be nil, but we have to handle it.
1178            destroy oldFunding
1179
1180            let enabledAutoRepayment = fusdProviderCapability != nil
1181
1182            emit FundingAvailable(
1183                fundingResourceID: fundingResourceID, 
1184                listingResourceID: listingResourceID, 
1185                borrower: borrower,
1186                lender: lender,
1187                nftID: nftID,
1188                nftType: nftType.identifier,
1189                repaymentAmount: repaymentAmount,
1190                enabledAutoRepayment: enabledAutoRepayment,
1191                repaymentAddress: fusdProviderCapability?.address
1192            )
1193
1194            return fundingResourceID
1195        }
1196
1197        // removeFunding
1198        // Remove a Funding.
1199        //
1200        access(Administrator) fun removeFunding(fundingResourceID: UInt64) {
1201            let funding <- self.fundings.remove(key: fundingResourceID)
1202                ?? panic("missing Funding")
1203    
1204            assert(funding.getDetails().repaid == true || funding.getDetails().settled == true, message: "funding is not repaid or settled")
1205
1206            if let callback = Flowty.borrowCallbackContainer() {
1207                callback.handle(stage: FlowtyListingCallback.Stage.Destroyed, listing: &funding as &{FlowtyListingCallback.Listing}, nft: nil)
1208            }
1209
1210            // This will emit a FundingCompleted event.
1211            Burner.burn(<-funding)
1212        }
1213
1214        // getFundingIDs
1215        // Returns an array of the Funding resource IDs that are in the collection
1216        //
1217        access(all) view fun getFundingIDs(): [UInt64] {
1218            return self.fundings.keys
1219        }
1220
1221        // borrowFunding
1222        // Returns a read-only view of the Funding for the given fundingID if it is contained by this collection.
1223        //
1224        access(all) view fun borrowFunding(fundingResourceID: UInt64): &{FundingPublic}? {
1225            return &self.fundings[fundingResourceID]
1226        }
1227
1228        // borrowPrivateFunding
1229        // Returns a private view of the Funding for the given fundingID if it is contained by this collection.
1230        //
1231        access(Administrator) view fun borrowPrivateFunding(fundingResourceID: UInt64): &Funding? {
1232            return &self.fundings[fundingResourceID]
1233        }
1234
1235        access(contract) fun burnCallback() {
1236            let ids = self.fundings.keys
1237            for id in ids {
1238                let funding <- self.fundings.remove(key: id)!
1239                Burner.burn(<- funding)
1240            }
1241
1242            // Let event consumers know that this marketplace will no longer exist
1243            emit FlowtyMarketplaceDestroyed(flowtyStorefrontResourceID: self.uuid)
1244        }
1245
1246        // constructor
1247        //
1248        init () {
1249            self.fundings <- {}
1250
1251            // Let event consumers know that this storefront exists
1252            emit FlowtyMarketplaceInitialized(flowtyMarketplaceResourceID: self.uuid)
1253        }
1254    }
1255
1256    // FlowtyStorefrontManager
1257    // An interface for adding and removing Listings within a FlowtyStorefront,
1258    // intended for use by the FlowtyStorefront's own
1259    access(all) resource interface FlowtyStorefrontManager {
1260        // createListing
1261        // Allows the FlowtyStorefront owner to create and insert Listings.
1262        //
1263        access(List) fun createListing(
1264            payment: @{FungibleToken.Vault},
1265            nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
1266            nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
1267            fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1268            nftType: Type,
1269            nftID: UInt64,
1270            amount: UFix64,
1271            interestRate: UFix64,
1272            term: UFix64,
1273            paymentVaultType: Type,
1274            paymentCuts: [PaymentCut],
1275            expiresAfter: UFix64
1276        ): UInt64
1277        // removeListing
1278        // Allows the FlowtyStorefront owner to remove any sale listing, accepted or not.
1279        //
1280        access(Cancel) fun removeListing(listingResourceID: UInt64)
1281    }
1282
1283    // FlowtyStorefrontPublic
1284    // An interface to allow listing and borrowing Listings, and funding loans via Listings
1285    // in a FlowtyStorefront.
1286    //
1287    access(all) resource interface FlowtyStorefrontPublic {
1288        access(all) view fun getListingIDs(): [UInt64]
1289        access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
1290            post {
1291                result == nil || result!.getType() == Type<@Listing>()
1292            }
1293        }
1294        access(all) fun cleanup(listingResourceID: UInt64)
1295        access(all) view fun getRoyalties(): {String:Flowty.Royalty}
1296   }
1297
1298    // FlowtyStorefront
1299    // A resource that allows its owner to manage a list of Listings, and anyone to interact with them
1300    // in order to query their details and fund the loans that they represent.
1301    //
1302    access(all) resource FlowtyStorefront : FlowtyStorefrontManager, FlowtyStorefrontPublic, Burner.Burnable {
1303        access(all) event ResourceDestroyed(flowtyStorefrontID: UInt64 = self.uuid)
1304
1305        // The dictionary of Listing uuids to Listing resources.
1306        access(self) var listings: @{UInt64: Listing}
1307
1308        // insert
1309        // Create and publish a Listing for an NFT.
1310        //
1311         access(List) fun createListing(
1312            payment: @{FungibleToken.Vault},
1313            nftProviderCapability: Capability<auth(NonFungibleToken.Withdraw) &{NonFungibleToken.Provider, NonFungibleToken.CollectionPublic}>,
1314            nftPublicCollectionCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
1315            fusdProviderCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider}>?,
1316            nftType: Type,
1317            nftID: UInt64,
1318            amount: UFix64,
1319            interestRate: UFix64,
1320            term: UFix64,
1321            paymentVaultType: Type,
1322            paymentCuts: [PaymentCut],
1323            expiresAfter: UFix64
1324         ): UInt64 {
1325             pre {
1326                // We don't allow all tokens to be used as payment. Check that the provided one is supported.
1327                FlowtyUtils.isTokenSupported(type: paymentVaultType): "provided payment type is not supported"
1328                // make sure that the FUSD vault has at least the listing fee
1329                payment.balance == Flowty.ListingFee: "payment vault does not contain requested listing fee amount"
1330                // check that the repayment token type is the same as the payment token if repayment is not nil
1331                fusdProviderCapability == nil || fusdProviderCapability!.check() && fusdProviderCapability!.borrow()!.getType() == paymentVaultType: "repayment vault type and payment vault type do not match"
1332                // There are no listing fees right now so this will ensure that no one attempts to send any
1333                payment.balance == 0.0: "no listing fee required"
1334                // make sure the payment type is the same as paymentVaultType
1335                payment.getType() == paymentVaultType: "payment type and paymentVaultType do not match"
1336                nftProviderCapability.check(): "invalid nft provider"
1337                term >= 1_209_600.0: "term must be at least 2 weeks"
1338            }
1339
1340            let nft = nftProviderCapability.borrow()!.borrowNFT(nftID) ?? panic("nft not found")
1341            assert(nft.getType() == nftType, message: "incorrect nft type")
1342            assert(FlowtyUtils.isSupported(nft), message: "nft type is not supported")
1343
1344            let royaltyRate = FlowtyUtils.getRoyaltyRate(nft)
1345
1346            let listing <- create Listing(
1347                nftProviderCapability: nftProviderCapability,
1348                nftPublicCollectionCapability: nftPublicCollectionCapability,
1349                fusdProviderCapability: fusdProviderCapability,
1350                nftType: nftType,
1351                nftID: nftID,
1352                amount: amount,
1353                interestRate: interestRate,
1354                term: term,
1355                paymentVaultType: paymentVaultType,
1356                paymentCuts: paymentCuts,
1357                flowtyStorefrontID: self.uuid,
1358                expiresAfter: expiresAfter,
1359                royaltyRate: royaltyRate
1360            )
1361
1362            if let callback = Flowty.borrowCallbackContainer() {
1363                callback.handle(stage: FlowtyListingCallback.Stage.Created, listing: &listing as &{FlowtyListingCallback.Listing}, nft: nft)
1364            }
1365
1366            let listingResourceID = listing.uuid
1367            let expiration = listing.getDetails().expiresAfter
1368
1369            // Add the new listing to the dictionary.
1370            let oldListing <- self.listings[listingResourceID] <- listing
1371            // Note that oldListing will always be nil, but we have to handle it.
1372            destroy oldListing
1373
1374            // Listing fee
1375            // let listingFee <- payment.withdraw(amount: Flowty.ListingFee)
1376            // let flowtyFusdReceiver = Flowty.account.borrow<&FUSD.Vault{FungibleToken.Receiver}>(from: Flowty.FusdVaultStoragePath)
1377            //     ?? panic("Missing or mis-typed FUSD Reveiver")
1378            // flowtyFusdReceiver.deposit(from: <-listingFee)
1379            destroy payment
1380
1381            let enabledAutoRepayment = fusdProviderCapability != nil
1382
1383            emit ListingAvailable(
1384                flowtyStorefrontAddress: self.owner?.address!,
1385                flowtyStorefrontID: self.uuid,
1386                listingResourceID: listingResourceID,
1387                nftType: nftType.identifier,
1388                nftID: nftID,
1389                amount: amount,
1390                interestRate: interestRate,
1391                term: term,
1392                enabledAutoRepayment: enabledAutoRepayment,
1393                royaltyRate: royaltyRate,
1394                expiresAfter: expiration,
1395                paymentTokenType: paymentVaultType.identifier,
1396                repaymentAddress: fusdProviderCapability?.address
1397            )
1398
1399            return listingResourceID
1400        }
1401
1402        // removeListing
1403        // Remove a Listing that has not yet been funded from the collection and destroy it.
1404        //
1405        access(Cancel) fun removeListing(listingResourceID: UInt64) {
1406            let listing <- self.listings.remove(key: listingResourceID)
1407                ?? panic("missing Listing")
1408
1409            if let callback = Flowty.borrowCallbackContainer() {
1410                callback.handle(stage: FlowtyListingCallback.Stage.Destroyed, listing: &listing as &{FlowtyListingCallback.Listing}, nft: nil)
1411            }
1412
1413            // This will emit a ListingCompleted event.
1414            Burner.burn(<- listing)
1415        }
1416
1417        // getListingIDs
1418        // Returns an array of the Listing resource IDs that are in the collection
1419        //
1420        access(all) view fun getListingIDs(): [UInt64] {
1421            return self.listings.keys
1422        }
1423
1424        // borrowListing
1425        // Returns a read-only view of the Listing for the given listingID if it is contained by this collection.
1426        //
1427        access(all) view fun borrowListing(listingResourceID: UInt64): &{ListingPublic}? {
1428            return &self.listings[listingResourceID]
1429        }
1430
1431        // cleanup
1432        // Remove an listing *if* it has been funded and expired.
1433        // Anyone can call, but at present it only benefits the account owner to do so.
1434        // Kind purchasers can however call it if they like.
1435        //
1436        access(all) fun cleanup(listingResourceID: UInt64) {
1437            pre {
1438                self.listings[listingResourceID] != nil: "could not find listing with given id"
1439            }
1440
1441            let listing <- self.listings.remove(key: listingResourceID)!
1442            assert(listing.getDetails().funded == true, message: "listing is not funded, only admin can remove")
1443            Burner.burn(<- listing)
1444        }
1445
1446        access(all) view fun getRoyalties(): {String:Flowty.Royalty} {
1447            return Flowty.Royalties
1448        }
1449
1450        // destructor
1451        //
1452        access(contract) fun burnCallback() {
1453            let ids = self.listings.keys
1454            for id in ids {
1455                Burner.burn(<- self.listings.remove(key: id))
1456            }
1457
1458            // Let event consumers know that this storefront will no longer exist
1459            emit FlowtyStorefrontDestroyed(flowtyStorefrontResourceID: self.uuid)
1460        }
1461
1462        // constructor
1463        //
1464        init () {
1465            self.listings <- {}
1466
1467            // Let event consumers know that this storefront exists
1468            emit FlowtyStorefrontInitialized(flowtyStorefrontResourceID: self.uuid)
1469        }
1470    }
1471
1472    // createStorefront
1473    // Make creating a FlowtyStorefront publicly accessible.
1474    //
1475    access(all) fun createStorefront(): @FlowtyStorefront {
1476        return <-create FlowtyStorefront()
1477    }
1478
1479    access(account) fun borrowMarketplace(): &Flowty.FlowtyMarketplace {
1480        return self.account.storage.borrow<&Flowty.FlowtyMarketplace>(from: Flowty.FlowtyMarketplaceStoragePath)!
1481    }
1482
1483    access(all) view fun borrowMarketplacePublic(): &{FlowtyMarketplacePublic} {
1484        let mp = self.account.capabilities.get<&{Flowty.FlowtyMarketplacePublic}>(Flowty.FlowtyMarketplacePublicPath)!.borrow()
1485            ?? panic("marketplace does not exist")
1486        return mp
1487    }
1488
1489    access(all) view fun getRoyaltySafe(nftTypeIdentifier: String): Royalty? {
1490        return Flowty.Royalties[nftTypeIdentifier]
1491    }
1492
1493    access(all) view fun getRoyalty(nftTypeIdentifier: String): Royalty {
1494        return Flowty.Royalties[nftTypeIdentifier]!
1495    }
1496
1497    access(all) view fun getTokenPaths(): {String:PublicPath} {
1498        return self.TokenPaths
1499    }
1500
1501    access(all) fun settleFunding(fundingResourceID: UInt64) {
1502       let marketplace = Flowty.borrowMarketplace()
1503       let funding = marketplace.borrowFunding(fundingResourceID: fundingResourceID)
1504       funding!.settleFunding()
1505    }
1506
1507    // FlowtyAdmin
1508    // Allows the adminitrator to set the amount of fees, set the suspended funding period
1509    //
1510    access(all) resource FlowtyAdmin {
1511        access(Administrator) fun setFees(listingFixedFee: UFix64, fundingPercentageFee: UFix64) {
1512            pre {
1513                // The UFix64 type covers a negative numbers
1514                fundingPercentageFee <= 1.0: "Funding fee should be a percentage"
1515            }
1516
1517            Flowty.ListingFee = listingFixedFee
1518            Flowty.FundingFee = fundingPercentageFee
1519        }
1520
1521        access(Administrator) fun setSuspendedFundingPeriod(period: UFix64) {
1522            Flowty.SuspendedFundingPeriod = period
1523        }
1524
1525        access(Administrator) fun setSupportedCollection(collection: String, state: Bool) {
1526            Flowty.SupportedCollections[collection] = state
1527            emit CollectionSupportChanged(collectionIdentifier: collection, state: state)
1528        }
1529        
1530        access(Administrator) fun setCollectionRoyalty(collection: String, royalty: Royalty) {
1531            pre {
1532                royalty.Rate <= 1.0: "Royalty rate must be a percentage"
1533            }
1534
1535            Flowty.Royalties[collection] = royalty
1536            emit RoyaltyAdded(collectionIdentifier: collection, rate: royalty.Rate)
1537        }
1538
1539        access(Administrator) fun registerFungibleTokenPath(vaultType: Type, path: PublicPath) {
1540            Flowty.TokenPaths[vaultType.identifier] = path
1541        }
1542    }
1543
1544    access(contract) fun borrowCallbackContainer(): auth(FlowtyListingCallback.Handle) &FlowtyListingCallback.Container? {
1545        return self.account.storage.borrow<auth(FlowtyListingCallback.Handle) &FlowtyListingCallback.Container>(from: FlowtyListingCallback.ContainerStoragePath)
1546    }
1547
1548    init () {
1549        self.FlowtyStorefrontStoragePath = /storage/FlowtyStorefront
1550        self.FlowtyStorefrontPublicPath = /public/FlowtyStorefront
1551        self.FlowtyMarketplaceStoragePath = /storage/FlowtyMarketplace
1552        self.FlowtyMarketplacePublicPath = /public/FlowtyMarketplace
1553        self.FlowtyAdminStoragePath = /storage/FlowtyAdmin
1554        self.FusdVaultStoragePath = /storage/fusdVault
1555        self.FusdReceiverPublicPath = /public/fusdReceiver
1556        self.FusdBalancePublicPath = /public/fusdBalance
1557
1558        self.ListingFee = 0.0 // Fixed FUSD
1559        self.FundingFee = 0.1 // Percentage of the interest, a number between 0 and 1.
1560        self.SuspendedFundingPeriod = 1.0 // Period in seconds
1561        self.Royalties = {}
1562        self.SupportedCollections = {}
1563        self.TokenPaths = {}
1564
1565        let marketplace <- create FlowtyMarketplace()
1566
1567        self.account.storage.save(<-marketplace, to: self.FlowtyMarketplaceStoragePath) 
1568        let mpPubCap = self.account.capabilities.storage.issue<&{Flowty.FlowtyMarketplacePublic}>(Flowty.FlowtyMarketplaceStoragePath)
1569        self.account.capabilities.publish(mpPubCap, at: Flowty.FlowtyMarketplacePublicPath)
1570
1571        // FlowtyAdmin
1572        let flowtyAdmin <- create FlowtyAdmin()
1573        self.account.storage.save(<-flowtyAdmin, to: self.FlowtyAdminStoragePath)
1574
1575        if self.account.storage.borrow<&AnyResource>(from: FlowtyListingCallback.ContainerStoragePath) == nil {
1576            let dnaHandler <- DNAHandler.createHandler()
1577            let listingHandler <- FlowtyListingCallback.createContainer(defaultHandler: <-dnaHandler)
1578            self.account.storage.save(<-listingHandler, to: FlowtyListingCallback.ContainerStoragePath)
1579        }
1580
1581        emit FlowtyInitialized()
1582    }
1583}
1584