Smart Contract

OffersV2

A.b8ea91944fd51c43.OffersV2

Valid From

85,985,161

Deployed

2d ago
Feb 24, 2026, 11:58:38 PM UTC

Dependents

46 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import DapperUtilityCoin from 0xead892083b3e2c6c
3import Resolver from 0xb8ea91944fd51c43
4import NonFungibleToken from 0x1d7e57aa55817448
5import MetadataViews from 0x1d7e57aa55817448
6import ViewResolver from 0x1d7e57aa55817448
7import Burner from 0xf233dcee88fe0abe
8
9/// OffersV2
10//
11// Contract holds the Offer resource and a public method to create them.
12//
13// Each Offer can have one or more royalties of the sale price that
14// goes to one or more addresses.
15//
16// Owners of NFT can watch for OfferAvailable events and check
17// the Offer amount to see if they wish to accept the offer.
18//
19// Marketplaces and other aggregators can watch for OfferAvailable events
20// and list offers of interest to logged in users.
21//
22access(all) contract OffersV2 {
23    // OfferAvailable
24    // An Offer has been created and added to the users DapperOffer resource.
25    //
26
27    access(all) event OfferAvailable(
28        offerAddress: Address,
29        offerId: UInt64,
30        nftType: Type,
31        offerAmount: UFix64,
32        royalties: {Address:UFix64},
33        offerType: String,
34        offerParamsString: {String:String},
35        offerParamsUFix64: {String:UFix64},
36        offerParamsUInt64: {String:UInt64},
37        paymentVaultType: Type,
38        resolverType: String,
39        paymentBorrowType: String
40    )
41
42    // OfferCompleted
43    // The Offer has been resolved. The offer has either been accepted
44    //  by the NFT owner, or the offer has been removed and destroyed.
45    //
46    access(all) event OfferCompleted(
47        purchased: Bool,
48        acceptingAddress: Address?,
49        offerAddress: Address,
50        offerId: UInt64,
51        nftType: Type,
52        offerAmount: UFix64,
53        royalties: {Address:UFix64},
54        offerType: String,
55        offerParamsString: {String:String},
56        offerParamsUFix64: {String:UFix64},
57        offerParamsUInt64: {String:UInt64},
58        paymentVaultType: Type,
59        nftId: UInt64?,
60    )
61
62    // Royalty
63    // A struct representing a recipient that must be sent a certain amount
64    // of the payment when a NFT is sold.
65    //
66    access(all) struct Royalty {
67        access(all) let receiver: Capability<&{FungibleToken.Receiver}>
68        access(all) let amount: UFix64
69
70        init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
71            self.receiver = receiver
72            self.amount = amount
73        }
74    }
75
76    // OfferDetails
77    // A struct containing Offers' data.
78    //
79    access(all) struct OfferDetails {
80        // The ID of the offer
81        access(all) let offerId: UInt64
82        // The Type of the NFT
83        access(all) let nftType: Type
84        // The Type of the FungibleToken that payments must be made in.
85        access(all) let paymentVaultType: Type
86        // The Offer amount for the NFT
87        access(all) let offerAmount: UFix64
88        // Flag to tracked the purchase state
89        access(all) var purchased: Bool
90        // This specifies the division of payment between recipients.
91        access(all) let royalties: [Royalty]
92        // Used to hold Offer metadata and offer type information
93        access(all) let offerParamsString: {String: String}
94        access(all) let offerParamsUFix64: {String:UFix64}
95        access(all) let offerParamsUInt64: {String:UInt64}
96
97        // setToPurchased
98        // Irreversibly set this offer as purchased.
99        //
100        access(contract) fun setToPurchased() {
101            self.purchased = true
102        }
103
104        // initializer
105        //
106        init(
107            offerId: UInt64,
108            nftType: Type,
109            offerAmount: UFix64,
110            royalties: [Royalty],
111            offerParamsString: {String: String},
112            offerParamsUFix64: {String:UFix64},
113            offerParamsUInt64: {String:UInt64},
114            paymentVaultType: Type,
115        ) {
116            self.offerId = offerId
117            self.nftType = nftType
118            self.offerAmount = offerAmount
119            self.purchased = false
120            self.royalties = royalties
121            self.offerParamsString = offerParamsString
122            self.offerParamsUFix64 = offerParamsUFix64
123            self.offerParamsUInt64 = offerParamsUInt64
124            self.paymentVaultType = paymentVaultType
125        }
126    }
127
128    // OfferPublic
129    // An interface providing a useful public interface to an Offer resource.
130    //
131    access(all) resource interface OfferPublic {
132        // accept
133        // This will accept the offer if provided with the NFT id that matches the Offer
134        //
135        access(all) fun accept(
136            item: @{NonFungibleToken.NFT, ViewResolver.Resolver},
137            receiverCapability: Capability<&{FungibleToken.Receiver}>,
138        ): Void
139        // getDetails
140        // Return Offer details
141        //
142        access(all) fun getDetails(): OfferDetails
143    }
144
145
146    access(all) resource Offer: OfferPublic, Burner.Burnable {
147
148        access(all)  event ResourceDestroyed(purchased: Bool = self.details.purchased, offerId: UInt64 = self.details.offerId)
149
150
151        // The OfferDetails struct of the Offer
152        access(self) let details: OfferDetails
153        // The vault which will handle the payment if the Offer is accepted.
154        access(contract) let vaultRefCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Provider, FungibleToken.Balance}>
155        // Receiver address for the NFT when/if the Offer is accepted.
156        access(contract) let nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>
157        // Resolver capability for the offer type
158        access(contract) let resolverCapability: Capability<&{Resolver.ResolverPublic}>
159
160        init(
161            vaultRefCapability: Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>,
162            nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
163            nftType: Type,
164            amount: UFix64,
165            royalties: [Royalty],
166            offerParamsString: {String: String},
167            offerParamsUFix64: {String:UFix64},
168            offerParamsUInt64: {String:UInt64},
169            resolverCapability: Capability<&{Resolver.ResolverPublic}>,
170        ) {
171            pre {
172                nftReceiverCapability.check(): "reward capability not valid"
173                resolverCapability.check(): "invalid resolver capability"
174            }
175            self.vaultRefCapability = vaultRefCapability
176            self.nftReceiverCapability = nftReceiverCapability
177            self.resolverCapability = resolverCapability
178            var price: UFix64 = amount
179            let royaltyInfo: {Address:UFix64} = {}
180
181            for royalty in royalties {
182                assert(royalty.receiver.check(), message: "invalid royalty receiver")
183                price = price - royalty.amount
184                royaltyInfo[royalty.receiver.address] = royalty.amount
185            }
186
187            assert(price > 0.0, message: "price must be > 0")
188
189            self.details = OfferDetails(
190                offerId: self.uuid,
191                nftType: nftType,
192                offerAmount: amount,
193                royalties: royalties,
194                offerParamsString: offerParamsString,
195                offerParamsUFix64: offerParamsUFix64,
196                offerParamsUInt64: offerParamsUInt64,
197                paymentVaultType: vaultRefCapability.getType(),
198            )
199
200            let resolver = self.resolverCapability.borrow() ?? panic("could not borrow resolver capability")
201            emit OfferAvailable(
202                offerAddress: nftReceiverCapability.address,
203                offerId: self.details.offerId,
204                nftType: self.details.nftType,
205                offerAmount: self.details.offerAmount,
206                royalties: royaltyInfo,
207                offerType: offerParamsString["_type"] ?? "unknown",
208                offerParamsString: self.details.offerParamsString,
209                offerParamsUFix64: self.details.offerParamsUFix64,
210                offerParamsUInt64: self.details.offerParamsUInt64,
211                paymentVaultType: self.details.paymentVaultType,
212                resolverType: resolver.getType().identifier,
213                paymentBorrowType: self.vaultRefCapability.borrow()?.getType()?.identifier ?? "unknown"
214            )
215        }
216
217        // accept
218        // Accept the offer if...
219        // - Calling from an Offer that hasn't been purchased/desetoryed.
220        // - Provided with a NFT matching the NFT id within the Offer details.
221        // - Provided with a NFT matching the NFT Type within the Offer details.
222        //
223        access(all) fun accept(
224                item: @{NonFungibleToken.NFT, ViewResolver.Resolver},
225                receiverCapability: Capability<&{FungibleToken.Receiver}>,
226            ): Void {
227
228            pre {
229                !self.details.purchased: "Offer has already been purchased"
230                item.isInstance(self.details.nftType): "item NFT is not of specified type"
231            }
232
233            let resolverCapability = self.resolverCapability.borrow() ?? panic("could not borrow resolver")
234            let resolverResult = resolverCapability.checkOfferResolver(
235                item: &item as &{NonFungibleToken.NFT, ViewResolver.Resolver},
236                offerParamsString: self.details.offerParamsString,
237                offerParamsUInt64: self.details.offerParamsUInt64,
238                offerParamsUFix64: self.details.offerParamsUFix64,
239            )
240
241            if !resolverResult {
242                panic("Resolver failed, invalid NFT please check Offer criteria")
243            }
244
245            self.details.setToPurchased()
246            let nft <- item as! @{NonFungibleToken.NFT}
247            let nftId: UInt64 = nft.id
248            self.nftReceiverCapability.borrow()!.deposit(token: <- nft)
249
250            let initalDucSupply = self.vaultRefCapability.borrow()!.balance
251            let payment <- self.vaultRefCapability.borrow()!.withdraw(amount: self.details.offerAmount)
252
253            // Payout royalties
254            for royalty in self.details.royalties {
255                if let receiver = royalty.receiver.borrow() {
256                    let amount = royalty.amount
257                    let part <- payment.withdraw(amount: amount)
258                    receiver.deposit(from: <- part)
259                }
260            }
261
262            receiverCapability.borrow()!.deposit(from: <- payment)
263
264            // If a DUC vault is being used for payment we must assert that no DUC is leaking from the transactions.
265            let isDucVault = self.vaultRefCapability.isInstance(
266                Type<Capability<&DapperUtilityCoin.Vault>>()
267            ) // todo: check if this is correct
268
269            if isDucVault {
270                assert(self.vaultRefCapability.borrow()!.balance == initalDucSupply, message: "DUC is leaking")
271            }
272
273            emit OfferCompleted(
274                purchased: self.details.purchased,
275                acceptingAddress: receiverCapability.address,
276                offerAddress: self.nftReceiverCapability.address,
277                offerId: self.details.offerId,
278                nftType: self.details.nftType,
279                offerAmount: self.details.offerAmount,
280                royalties: self.getRoyaltyInfo(),
281                offerType: self.details.offerParamsString["_type"] ?? "unknown",
282                offerParamsString: self.details.offerParamsString,
283                offerParamsUFix64: self.details.offerParamsUFix64,
284                offerParamsUInt64: self.details.offerParamsUInt64,
285                paymentVaultType: self.details.paymentVaultType,
286                nftId: nftId,
287            )
288        }
289
290        // getDetails
291        // Return Offer details
292        //
293        access(all) view fun getDetails(): OfferDetails {
294            return self.details
295        }
296
297        // getRoyaltyInfo
298        // Return royalty details
299        //
300        access(all) view fun getRoyaltyInfo(): {Address:UFix64} {
301            let royaltyInfo: {Address:UFix64} = {}
302
303            for royalty in self.details.royalties {
304                royaltyInfo[royalty.receiver.address] = royalty.amount
305            }
306            return royaltyInfo;
307        }
308
309        access(contract) fun burnCallback() {
310            // Only emit the event if the offer has not been purchased; otherwise, the event has already been emitted
311            if !self.details.purchased {
312                emit OfferCompleted(
313                    purchased: false,
314                    acceptingAddress: nil,
315                    offerAddress: self.nftReceiverCapability.address,
316                    offerId: self.details.offerId,
317                    nftType: self.details.nftType,
318                    offerAmount: self.details.offerAmount,
319                    royalties: self.getRoyaltyInfo(),
320                    offerType: self.details.offerParamsString["_type"] ?? "unknown",
321                    offerParamsString: self.details.offerParamsString,
322                    offerParamsUFix64: self.details.offerParamsUFix64,
323                    offerParamsUInt64: self.details.offerParamsUInt64,
324                    paymentVaultType: self.details.paymentVaultType,
325                    nftId: nil,
326                )
327            }
328        }
329    }
330
331    // makeOffer
332    access(all) fun makeOffer(
333        vaultRefCapability:  Capability<auth(FungibleToken.Withdraw) &{FungibleToken.Vault}>,
334        nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
335        nftType: Type,
336        amount: UFix64,
337        royalties: [Royalty],
338        offerParamsString: {String:String},
339        offerParamsUFix64: {String:UFix64},
340        offerParamsUInt64: {String:UInt64},
341        resolverCapability: Capability<&{Resolver.ResolverPublic}>,
342    ): @Offer {
343        let newOfferResource <- create Offer(
344            vaultRefCapability: vaultRefCapability,
345            nftReceiverCapability: nftReceiverCapability,
346            nftType: nftType,
347            amount: amount,
348            royalties: royalties,
349            offerParamsString: offerParamsString,
350            offerParamsUFix64: offerParamsUFix64,
351            offerParamsUInt64: offerParamsUInt64,
352            resolverCapability: resolverCapability,
353        )
354        return <-newOfferResource
355    }
356}