Smart Contract
OffersV2
A.b8ea91944fd51c43.OffersV2
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}