Smart Contract

StarlyCardBidV3

A.5b82f21c0edf76e3.StarlyCardBidV3

Deployed

17h ago
Feb 28, 2026, 02:30:51 AM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import StarlyIDParser from 0x5b82f21c0edf76e3
4import StakedStarlyCard from 0x29fcd0b5e444242a
5import StarlyCard from 0x5b82f21c0edf76e3
6import StarlyCardMarket from 0x5b82f21c0edf76e3
7import StarlyCardStaking from 0x29fcd0b5e444242a
8import StarlyRoyalties from 0x5b82f21c0edf76e3
9
10pub contract StarlyCardBidV3 {
11
12    pub var totalCount: UInt64
13
14    pub event StarlyCardBidCreated(
15        bidID: UInt64,
16        nftID: UInt64,
17        starlyID: String,
18        bidPrice: UFix64,
19        bidVaultType: String,
20        bidderAddress: Address,
21        beneficiarySaleCut: StarlyCardMarket.SaleCut,
22        creatorSaleCut: StarlyCardMarket.SaleCut,
23        additionalSaleCuts: [StarlyCardMarket.SaleCut])
24
25    pub event StarlyCardBidAccepted(
26        bidID: UInt64,
27        nftID: UInt64,
28        starlyID: String)
29
30    // Declined by the card owner
31    pub event StarlyCardBidDeclined(
32        bidID: UInt64,
33        nftID: UInt64,
34        starlyID: String)
35
36    // Bid cancelled by the bidder
37    pub event StarlyCardBidCancelled(
38        bidID: UInt64,
39        nftID: UInt64,
40        starlyID: String)
41
42    // Bid invalidated due to changed conditions, i.e. remaining resource
43    pub event StarlyCardBidInvalidated(
44        bidID: UInt64,
45        nftID: UInt64,
46        starlyID: String)
47
48    // Paths
49    pub let CollectionStoragePath: StoragePath
50    pub let CollectionPublicPath: PublicPath
51
52    pub resource interface BidPublicView {
53        pub let id: UInt64
54        pub let nftID: UInt64
55        pub let starlyID: String
56        pub let remainingResource: UFix64
57        pub let bidPrice: UFix64
58        pub let bidVaultType: Type
59    }
60
61    pub resource Bid: BidPublicView {
62        pub let id: UInt64
63
64        pub let nftID: UInt64
65        pub let starlyID: String
66
67        // card's remainig resource
68        pub let remainingResource: UFix64
69
70        // The price offered by the bidder
71        pub let bidPrice: UFix64
72        // The Type of the FungibleToken that payments must be made in
73        pub let bidVaultType: Type
74        access(self) let bidVault: @FungibleToken.Vault
75
76        pub let bidderAddress: Address
77        access(self) let bidderFungibleReceiver: Capability<&{FungibleToken.Receiver}>
78
79        access(self) let beneficiarySaleCutReceiver: StarlyCardMarket.SaleCutReceiverV2
80        access(self) let creatorSaleCutReceiver: StarlyCardMarket.SaleCutReceiverV2
81        access(self) let additionalSaleCutReceivers: [StarlyCardMarket.SaleCutReceiverV2]
82
83        pub fun accept(
84            bidderCardCollection: &StarlyCard.Collection{StarlyCard.StarlyCardCollectionPublic},
85            sellerFungibleReceiver: Capability<&{FungibleToken.Receiver}>,
86            sellerCardCollection: Capability<&StarlyCard.Collection{NonFungibleToken.Provider}>,
87            sellerStakedCardCollection: &StakedStarlyCard.Collection,
88            sellerMarketCollection: &StarlyCardMarket.Collection,
89            cardStakeId: UInt64?
90        ) {
91            pre {
92                self.bidVault.balance == self.bidPrice: "The amount of locked funds is incorrect"
93            }
94
95            let currentRemainingResource = StarlyCardStaking.getRemainingResourceWithDefault(starlyID: self.starlyID)
96            if currentRemainingResource != self.remainingResource {
97                emit StarlyCardBidInvalidated(bidID: self.id, nftID: self.nftID, starlyID: self.starlyID)
98                return
99            }
100
101            if let stakeId = cardStakeId {
102                self.unstakeCardIfStaked(sellerStakedCardCollection: sellerStakedCardCollection, stakeId: stakeId)
103                self.removeMarketSaleOfferIfExists(sellerMarketCollection: sellerMarketCollection)
104            }
105
106            // sale cuts
107            let beneficiaryCutAmount = self.bidPrice * self.beneficiarySaleCutReceiver.percent
108            let beneficiaryCut <- self.bidVault.withdraw(amount: beneficiaryCutAmount)
109            self.beneficiarySaleCutReceiver.receiver.borrow()!.deposit(from: <- beneficiaryCut)
110
111            let creatorCutAmount = self.bidPrice * self.creatorSaleCutReceiver.percent
112            let creatorCut <- self.bidVault.withdraw(amount: creatorCutAmount)
113            self.creatorSaleCutReceiver.receiver.borrow()!.deposit(from: <- creatorCut)
114
115            for additionalSaleCutReceiver in self.additionalSaleCutReceivers {
116                let additionalCutAmount = self.bidPrice * additionalSaleCutReceiver.percent
117                let additionalCut <- self.bidVault.withdraw(amount: additionalCutAmount)
118                additionalSaleCutReceiver.receiver.borrow()!.deposit(from: <- additionalCut)
119            }
120
121            // The rest goes to the seller
122            let sellerCutAmount = self.bidVault.balance
123            sellerFungibleReceiver.borrow()!.deposit(from: <- self.bidVault.withdraw(amount: sellerCutAmount))
124
125            // transfer card
126            let nft <- sellerCardCollection.borrow()!.withdraw(withdrawID: self.nftID)
127            bidderCardCollection.deposit(token: <- nft)
128
129            emit StarlyCardBidAccepted(
130                bidID: self.id,
131                nftID: self.nftID,
132                starlyID: self.starlyID)
133        }
134
135        access(self) fun removeMarketSaleOfferIfExists(sellerMarketCollection: &StarlyCardMarket.Collection) {
136            let marketOfferIds = sellerMarketCollection.getSaleOfferIDs()
137            if marketOfferIds.contains(self.nftID) {
138                let offer <- sellerMarketCollection.remove(itemID: self.nftID)
139                destroy offer
140            }
141        }
142
143        access(self) fun unstakeCardIfStaked(sellerStakedCardCollection: &StakedStarlyCard.Collection, stakeId: UInt64) {
144            let cardStake = sellerStakedCardCollection.borrowStakePrivate(id: stakeId)
145            if cardStake.getStarlyID() == self.starlyID {
146                sellerStakedCardCollection.unstake(id: stakeId)
147            }
148        }
149
150        init(
151            nftID: UInt64,
152            starlyID: String,
153            bidPrice: UFix64,
154            bidVaultType: Type,
155            bidderAddress: Address,
156            bidderFungibleReceiver: Capability<&{FungibleToken.Receiver}>,
157            bidderFungibleProvider: &AnyResource{FungibleToken.Provider},
158            beneficiarySaleCutReceiver: StarlyCardMarket.SaleCutReceiverV2,
159            creatorSaleCutReceiver: StarlyCardMarket.SaleCutReceiverV2,
160            additionalSaleCutReceivers: [StarlyCardMarket.SaleCutReceiverV2],
161        ) {
162            pre {
163                bidPrice > 0.0: "The bid price must be non zero"
164                bidderFungibleProvider.isInstance(bidVaultType): "Wrong Bid fungible provider type"
165                StarlyCardMarket.checkSaleCutReceiverV2(saleCutReceiver: beneficiarySaleCutReceiver): "Cannot borrow receiver in beneficiarySaleCutReceiver"
166                StarlyCardMarket.checkSaleCutReceiverV2(saleCutReceiver: creatorSaleCutReceiver): "Cannot borrow receiver in creatorSaleCutReceiver"
167                StarlyCardMarket.checkSaleCutReceiversV2(saleCutReceivers: additionalSaleCutReceivers): "Cannot borrow receiver in additionalSaleCutReceivers"
168            }
169
170            self.id = StarlyCardBidV3.totalCount
171            self.nftID = nftID
172            self.starlyID = starlyID
173            self.bidPrice = bidPrice
174            self.bidVaultType = bidVaultType
175            self.bidderAddress = bidderAddress
176            self.bidderFungibleReceiver = bidderFungibleReceiver
177            self.beneficiarySaleCutReceiver = beneficiarySaleCutReceiver
178            self.creatorSaleCutReceiver = creatorSaleCutReceiver
179            self.additionalSaleCutReceivers = additionalSaleCutReceivers
180
181            self.remainingResource = StarlyCardStaking.getRemainingResourceWithDefault(starlyID: starlyID)
182
183            let beneficiaryCutAmount = self.bidPrice * self.beneficiarySaleCutReceiver.percent
184            let creatorCutAmount = self.bidPrice * self.creatorSaleCutReceiver.percent
185            var additionalSaleCutAmountSum = 0.0
186            var additionalSaleCuts: [StarlyCardMarket.SaleCut] = []
187            for additionalSaleCutReceiver in self.additionalSaleCutReceivers {
188            let additionalCutAmount = self.bidPrice * additionalSaleCutReceiver.percent
189                additionalSaleCutAmountSum = additionalSaleCutAmountSum + additionalCutAmount
190                additionalSaleCuts.append(StarlyCardMarket.SaleCut(
191                    address: additionalSaleCutReceiver.receiver.address,
192                    amount: additionalCutAmount,
193                    percent: additionalSaleCutReceiver.percent));
194            }
195
196            self.bidVault <- bidderFungibleProvider.withdraw(amount: bidPrice)
197
198            StarlyCardBidV3.totalCount = StarlyCardBidV3.totalCount + (1 as UInt64)
199
200            emit StarlyCardBidCreated(
201                bidID: self.id,
202                nftID: self.nftID,
203                starlyID: self.starlyID,
204                bidPrice: self.bidPrice,
205                bidVaultType: self.bidVaultType.identifier,
206                bidderAddress: self.bidderAddress,
207                beneficiarySaleCut: StarlyCardMarket.SaleCut(
208                    address: self.beneficiarySaleCutReceiver.receiver.address,
209                    amount: beneficiaryCutAmount,
210                    percent: self.beneficiarySaleCutReceiver.percent),
211                creatorSaleCut: StarlyCardMarket.SaleCut(
212                    address: self.creatorSaleCutReceiver.receiver.address,
213                    amount: creatorCutAmount,
214                    percent: self.creatorSaleCutReceiver.percent),
215                additionalSaleCuts: additionalSaleCuts
216            )
217        }
218
219        destroy () {
220            if self.bidVault.balance > 0.0 {
221                // return back to the bidder
222                self.bidderFungibleReceiver.borrow()!.deposit(from: <- self.bidVault)
223            } else {
224                destroy self.bidVault
225            }
226        }
227    }
228
229    pub resource interface CollectionManager {
230        pub fun insert(bid: @StarlyCardBidV3.Bid)
231        pub fun remove(bidID: UInt64): @Bid
232        pub fun cancel(bidID: UInt64)
233    }
234
235    pub resource interface CollectionPublic {
236        pub fun getBidIDs(): [UInt64]
237
238        pub fun borrowBid(bidID: UInt64): &Bid{BidPublicView}?
239
240        pub fun accept(
241            bidID: UInt64,
242            bidderCardCollection: &StarlyCard.Collection{StarlyCard.StarlyCardCollectionPublic},
243            sellerFungibleReceiver: Capability<&{FungibleToken.Receiver}>,
244            sellerCardCollection: Capability<&StarlyCard.Collection{NonFungibleToken.Provider}>,
245            sellerStakedCardCollection: &StakedStarlyCard.Collection,
246            sellerMarketCollection: &StarlyCardMarket.Collection,
247            cardStakeId: UInt64?)
248
249        pub fun decline(bidID: UInt64)
250    }
251
252    pub resource Collection : CollectionManager, CollectionPublic {
253        pub var bids: @{UInt64: Bid}
254
255        pub fun getBidIDs(): [UInt64] {
256            return self.bids.keys
257        }
258
259        pub fun borrowBid(bidID: UInt64): &Bid{BidPublicView}? {
260            if self.bids[bidID] == nil {
261                return nil
262            }
263
264            return &self.bids[bidID] as &Bid{BidPublicView}?
265        }
266
267        pub fun insert(bid: @StarlyCardBidV3.Bid) {
268            let oldBid <- self.bids[bid.id] <- bid
269            destroy oldBid
270        }
271
272        pub fun remove(bidID: UInt64): @Bid {
273            return <- (self.bids.remove(key: bidID) ?? panic("missing bid"))
274        }
275
276        pub fun accept(
277            bidID: UInt64,
278            bidderCardCollection: &StarlyCard.Collection{StarlyCard.StarlyCardCollectionPublic},
279            sellerFungibleReceiver: Capability<&{FungibleToken.Receiver}>,
280            sellerCardCollection: Capability<&StarlyCard.Collection{NonFungibleToken.Provider}>,
281            sellerStakedCardCollection: &StakedStarlyCard.Collection,
282            sellerMarketCollection: &StarlyCardMarket.Collection,
283            cardStakeId: UInt64?
284        ) {
285            let bid <- self.remove(bidID: bidID)
286            bid.accept(
287                bidderCardCollection: bidderCardCollection,
288                sellerFungibleReceiver: sellerFungibleReceiver,
289                sellerCardCollection: sellerCardCollection,
290                sellerStakedCardCollection: sellerStakedCardCollection,
291                sellerMarketCollection: sellerMarketCollection,
292                cardStakeId: cardStakeId
293            )
294            destroy bid
295        }
296
297        pub fun decline(bidID: UInt64) {
298            let bid <- self.remove(bidID: bidID)
299            emit StarlyCardBidDeclined(bidID: bidID, nftID: bid.nftID, starlyID: bid.starlyID)
300            destroy bid
301        }
302
303        pub fun cancel(bidID: UInt64) {
304            let bid <- self.remove(bidID: bidID)
305            emit StarlyCardBidCancelled(bidID: bidID, nftID: bid.nftID, starlyID: bid.starlyID)
306            destroy bid
307        }
308
309        destroy () {
310            destroy self.bids
311        }
312
313        init () {
314            self.bids <- {}
315        }
316    }
317
318    pub fun createBid(
319        nftID: UInt64,
320        starlyID: String,
321        bidPrice: UFix64,
322        bidVaultType: Type,
323        bidderAddress: Address,
324        bidderFungibleReceiver: Capability<&{FungibleToken.Receiver}>,
325        bidderFungibleProvider: &AnyResource{FungibleToken.Provider},
326        beneficiarySaleCutReceiver: StarlyCardMarket.SaleCutReceiverV2,
327        creatorSaleCutReceiver: StarlyCardMarket.SaleCutReceiverV2,
328        minterSaleCutReceiver: StarlyCardMarket.SaleCutReceiverV2,
329    ): @Bid {
330        let parsedStarlyID = StarlyIDParser.parse(starlyID: starlyID)
331        let collectionID = parsedStarlyID.collectionID
332        let starlyRoyalty = StarlyRoyalties.getStarlyRoyalty()
333        let collectionRoyalty = StarlyRoyalties.getCollectionRoyalty(collectionID: collectionID)
334            ?? panic("Could not get creator royalty")
335        let minterRoyalty = StarlyRoyalties.getMinterRoyalty(collectionID: collectionID, starlyID: starlyID)
336            ?? panic("Could not get minter royalty")
337
338        assert(beneficiarySaleCutReceiver.receiver.address == starlyRoyalty.address, message: "Incorrect Starly royalty address")
339        assert(creatorSaleCutReceiver.receiver.address == collectionRoyalty.address, message: "Incorrect creator royalty address")
340        assert(minterSaleCutReceiver.receiver.address == minterRoyalty.address, message: "Incorrect minter royalty address")
341
342        assert(beneficiarySaleCutReceiver.percent == starlyRoyalty.cut, message: "Incorrect Starly royalty percent")
343        assert(creatorSaleCutReceiver.percent == collectionRoyalty.cut, message: "Incorrect creator royalty percent")
344        assert(minterSaleCutReceiver.percent == minterRoyalty.cut, message: "Incorrect minter royalty percent")
345
346        return <- create Bid(
347            nftID: nftID,
348            starlyID: starlyID,
349            bidPrice: bidPrice,
350            bidVaultType: bidVaultType,
351            bidderAddress: bidderAddress,
352            bidderFungibleReceiver: bidderFungibleReceiver,
353            bidderFungibleProvider: bidderFungibleProvider,
354            beneficiarySaleCutReceiver: beneficiarySaleCutReceiver,
355            creatorSaleCutReceiver: creatorSaleCutReceiver,
356            additionalSaleCutReceivers: [minterSaleCutReceiver],
357        )
358    }
359
360    pub fun createEmptyCollection(): @Collection {
361        return <- create Collection()
362    }
363
364    init() {
365        self.totalCount = 0
366        self.CollectionStoragePath = /storage/starlyCardBidV3Collection
367        self.CollectionPublicPath = /public/starlyCardBidV3Collection
368    }
369}
370