Smart Contract

Offers

A.b8ea91944fd51c43.Offers

Deployed

1d ago
Feb 26, 2026, 09:44:00 PM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import DapperUtilityCoin from 0xead892083b3e2c6c
4
5// Offers
6//
7// Contract holds the Offer resource and a public method to create them.
8//
9// Each Offer can have one or more royalties of the sale price that
10// goes to one or more addresses.
11//
12// Owners of NFT can watch for OfferAvailable events and check
13// the Offer amount to see if they wish to accept the offer.
14//
15// Marketplaces and other aggregators can watch for OfferAvailable events
16// and list offers of interest to logged in users.
17//
18pub contract Offers {
19    // OfferAvailable
20    // An Offer has been created and added to the users DapperOffer resource.
21    //
22    pub event OfferAvailable(
23        offerAddress: Address,
24        offerId: UInt64,
25        nftType: Type,
26        nftId: UInt64,
27        offerAmount: UFix64,
28        royalties: {Address:UFix64},
29    )
30
31    // OfferCompleted
32    // The Offer has been resolved. The offer has either been accepted
33    //  by the NFT owner, or the offer has been removed and destroyed.
34    //
35    pub event OfferCompleted(
36        offerId: UInt64,
37        nftType: Type,
38        nftId: UInt64,
39        purchased: Bool,
40        acceptingAddress: Address?,
41    )
42
43    // Royalty
44    // A struct representing a recipient that must be sent a certain amount
45    // of the payment when a NFT is sold.
46    //
47    pub struct Royalty {
48        pub let receiver: Capability<&{FungibleToken.Receiver}>
49        pub let amount: UFix64
50
51        init(receiver: Capability<&{FungibleToken.Receiver}>, amount: UFix64) {
52            self.receiver = receiver
53            self.amount = amount
54        }
55    }
56
57    // OfferDetails
58    // A struct containing Offers' data.
59    //
60    pub struct OfferDetails {
61        // The ID of the offer
62        pub let offerId: UInt64
63        // The Type of the NFT
64        pub let nftType: Type
65        // The ID of the NFT
66        pub let nftId: UInt64
67        // The Offer amount for the NFT
68        pub let offerAmount: UFix64
69        // Flag to tracked the purchase state
70        pub var purchased: Bool
71        // This specifies the division of payment between recipients.
72        pub let royalties: [Royalty]
73
74        // setToPurchased
75        // Irreversibly set this offer as purchased.
76        //
77        access(contract) fun setToPurchased() {
78            self.purchased = true
79        }
80
81        // initializer
82        //
83        init(
84            offerId: UInt64,
85            nftType: Type,
86            nftId: UInt64,
87            offerAmount: UFix64,
88            royalties: [Royalty]
89        ) {
90            self.offerId = offerId
91            self.nftType = nftType
92            self.nftId = nftId
93            self.offerAmount = offerAmount
94            self.purchased = false
95            self.royalties = royalties
96        }
97    }
98
99    // OfferPublic
100    // An interface providing a useful public interface to an Offer resource.
101    //
102    pub resource interface OfferPublic {
103        // accept
104        // This will accept the offer if provided with the NFT id that matches the Offer
105        //
106        pub fun accept(
107            item: @NonFungibleToken.NFT,
108            receiverCapability: Capability<&{FungibleToken.Receiver}>
109        ): Void
110        // getDetails
111        // Return Offer details
112        //
113        pub fun getDetails(): OfferDetails
114    }
115
116    pub resource Offer: OfferPublic {
117        // The OfferDetails struct of the Offer
118        access(self) let details: OfferDetails
119        // The vault which will handle the payment if the Offer is accepted.
120        access(contract) let vaultRefCapability: Capability<&{FungibleToken.Provider, FungibleToken.Balance}>
121        // Receiver address for the NFT when/if the Offer is accepted.
122        access(contract) let nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>
123
124        init(
125            vaultRefCapability: Capability<&{FungibleToken.Provider, FungibleToken.Balance}>,
126            nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
127            nftType: Type,
128            nftId: UInt64,
129            amount: UFix64,
130            royalties: [Royalty],
131        ) {
132            pre {
133                nftReceiverCapability.check(): "reward capability not valid"
134            }
135            self.vaultRefCapability = vaultRefCapability
136            self.nftReceiverCapability = nftReceiverCapability
137
138            var price: UFix64 = amount
139            let royaltyInfo: {Address:UFix64} = {}
140
141            for royalty in royalties {
142                assert(royalty.receiver.check(), message: "invalid royalty receiver")
143                price = price - royalty.amount
144                royaltyInfo[royalty.receiver.address] = royalty.amount
145            }
146
147            assert(price > 0.0, message: "price must be > 0")
148
149            self.details = OfferDetails(
150                offerId: self.uuid,
151                nftType: nftType,
152                nftId: nftId,
153                offerAmount: amount,
154                royalties: royalties
155            )
156
157            emit OfferAvailable(
158                offerAddress: nftReceiverCapability.address,
159                offerId: self.details.offerId,
160                nftType: self.details.nftType,
161                nftId: self.details.nftId,
162                offerAmount: self.details.offerAmount,
163                royalties: royaltyInfo,
164            )
165        }
166
167        // accept
168        // Accept the offer if...
169        // - Calling from an Offer that hasn't been purchased/desetoryed.
170        // - Provided with a NFT matching the NFT id within the Offer details.
171        // - Provided with a NFT matching the NFT Type within the Offer details.
172        //
173        pub fun accept(
174                item: @NonFungibleToken.NFT,
175                receiverCapability: Capability<&{FungibleToken.Receiver}>
176            ): Void {
177
178            pre {
179                !self.details.purchased: "Offer has already been purchased"
180                item.id == self.details.nftId: "item NFT does not have specified ID"
181                item.isInstance(self.details.nftType): "item NFT is not of specified type"
182            }
183
184            self.details.setToPurchased()
185            self.nftReceiverCapability.borrow()!.deposit(token: <- item)
186
187            let initalDucSupply = self.vaultRefCapability.borrow()!.balance
188            let payment <- self.vaultRefCapability.borrow()!.withdraw(amount: self.details.offerAmount)
189
190            // Payout royalties
191            for royalty in self.details.royalties {
192                if let receiver = royalty.receiver.borrow() {
193                    let amount = royalty.amount
194                    let part <- payment.withdraw(amount: amount)
195                    receiver.deposit(from: <- part)
196                }
197            }
198
199            receiverCapability.borrow()!.deposit(from: <- payment)
200
201            // If a DUC vault is being used for payment we must assert that no DUC is leaking from the transactions.
202            let isDucVault = self.vaultRefCapability.isInstance(
203                Type<Capability<&DapperUtilityCoin.Vault{FungibleToken.Provider, FungibleToken.Balance}>>()
204            )
205
206            if isDucVault {
207                assert(self.vaultRefCapability.borrow()!.balance == initalDucSupply, message: "DUC is leaking")
208            }
209
210            emit OfferCompleted(
211                offerId: self.details.offerId,
212                nftType: self.details.nftType,
213                nftId: self.details.nftId,
214                purchased: self.details.purchased,
215                acceptingAddress: receiverCapability.address,
216            )
217        }
218
219        // getDetails
220        // Return Offer details
221        //
222        pub fun getDetails(): OfferDetails {
223            return self.details
224        }
225
226        destroy() {
227            if !self.details.purchased {
228                emit OfferCompleted(
229                    offerId: self.details.offerId,
230                    nftType: self.details.nftType,
231                    nftId: self.details.nftId,
232                    purchased: self.details.purchased,
233                    acceptingAddress: nil,
234                )
235            }
236        }
237    }
238
239    // makeOffer
240    pub fun makeOffer(
241        vaultRefCapability: Capability<&{FungibleToken.Provider, FungibleToken.Balance}>,
242        nftReceiverCapability: Capability<&{NonFungibleToken.CollectionPublic}>,
243        nftType: Type,
244        nftId: UInt64,
245        amount: UFix64,
246        royalties: [Royalty],
247    ): @Offer {
248        let newOfferResource <- create Offer(
249            vaultRefCapability: vaultRefCapability,
250            nftReceiverCapability: nftReceiverCapability,
251            nftType: nftType,
252            nftId: nftId,
253            amount: amount,
254            royalties: royalties,
255        )
256        return <-newOfferResource
257    }
258}
259