Smart Contract

TopShotEscrowV2

A.4eded0de73020ca5.TopShotEscrowV2

Deployed

1w ago
Feb 19, 2026, 08:41:48 AM UTC

Dependents

0 imports
1import FungibleToken from 0xf233dcee88fe0abe
2import NonFungibleToken from 0x1d7e57aa55817448
3import TopShot from 0x0b2a3299cc857e29
4
5pub contract TopShotEscrowV2 {
6
7    // -----------------------------------------------------------------------
8    // TopShotEscrowV2 contract Events
9    // -----------------------------------------------------------------------
10
11    pub event ContractInitialized()
12    pub event Escrowed(id: UInt64, owner: Address, NFTIds: [UInt64], duration: UFix64, startTime: UFix64)
13    pub event Redeemed(id: UInt64, owner: Address, NFTIds: [UInt64], partial: Bool, time: UFix64)
14    pub event EscrowCancelled(id: UInt64, owner: Address, NFTIds: [UInt64], partial: Bool, time: UFix64)
15    pub event EscrowWithdraw(id: UInt64, from: Address?)
16    pub event EscrowUpdated(id: UInt64, owner: Address, NFTIds: [UInt64])
17
18    // -----------------------------------------------------------------------
19    // TopShotEscrowV2 contract-level fields.
20    // These contain actual values that are stored in the smart contract.
21    // -----------------------------------------------------------------------
22
23    // The total amount of EscrowItems that have been created
24    pub var totalEscrows: UInt64
25
26    // Escrow Storage Path
27    pub let escrowStoragePath: StoragePath
28
29    /// Escrow Public Path
30    pub let escrowPublicPath: PublicPath
31
32    // -----------------------------------------------------------------------
33    // TopShotEscrowV2 contract-level Composite Type definitions
34    // -----------------------------------------------------------------------
35    // These are just *definitions* for Types that this contract
36    // and other accounts can use. These definitions do not contain
37    // actual stored values, but an instance (or object) of one of these Types
38    // can be created by this contract that contains stored values.
39    // -----------------------------------------------------------------------
40
41    // This struct contains the status of the escrow
42    // and is exposed so websites can use escrow information
43    pub struct EscrowDetails {
44        pub let owner: Address
45        pub let escrowID: UInt64
46        pub let NFTIds: [UInt64]?
47        pub let starTime: UFix64
48        pub let duration : UFix64
49        pub let isRedeemable : Bool
50        
51        init(_owner: Address,
52            _escrowID: UInt64, 
53            _NFTIds: [UInt64]?,
54            _startTime: UFix64,
55            _duration: UFix64, 
56            _isRedeemable: Bool
57        ) {
58            self.owner = _owner
59            self.escrowID = _escrowID
60            self.NFTIds = _NFTIds
61            self.starTime = _startTime
62            self.duration = _duration
63            self.isRedeemable = _isRedeemable
64        }
65    }
66
67    // An interface that exposes public fields and functions
68    // of the EscrowItem resource
69    pub resource interface EscrowItemPublic {
70        pub let escrowID: UInt64
71        pub var redeemed: Bool
72        pub fun hasBeenRedeemed(): Bool
73        pub fun isRedeemable(): Bool
74        pub fun getEscrowDetails(): EscrowDetails
75        pub fun redeem(NFTIds: [UInt64])
76        pub fun addNFTs(NFTCollection: @TopShot.Collection)
77    }
78
79    // EscrowItem contains a NFT Collection (single or several NFTs) for a single escrow
80    // Fields and functions are defined as private by default
81    // to access escrow details, one can call getEscrowDetails()
82    pub resource EscrowItem: EscrowItemPublic {
83
84        // The id of this individual escrow
85        pub let escrowID: UInt64
86        access(self) var NFTCollection: [UInt64]?
87        pub let startTime:  UFix64
88        access(self) var duration: UFix64
89        pub var redeemed: Bool
90        access(self) let receiverCap : Capability<&{NonFungibleToken.Receiver}>
91        access(self) var lock: Bool
92
93        init(
94            _NFTCollection: @TopShot.Collection,
95            _duration: UFix64,
96            _receiverCap : Capability<&{NonFungibleToken.Receiver}> 
97        ) {
98            TopShotEscrowV2.totalEscrows = TopShotEscrowV2.totalEscrows + 1
99            self.escrowID =  TopShotEscrowV2.totalEscrows
100            self.NFTCollection = _NFTCollection.getIDs()
101            assert(self.NFTCollection != nil, message: "NFT Collection is empty")
102            self.startTime = getCurrentBlock().timestamp
103            self.duration = _duration
104            assert(_receiverCap.borrow() != nil, message: "Cannot borrow receiver")
105            self.receiverCap = _receiverCap
106            self.redeemed = false
107            self.lock = false
108
109            let adminTopShotReceiverRef = TopShotEscrowV2.account.getCapability(/public/MomentCollection).borrow<&{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, TopShot.MomentCollectionPublic}>()
110            ?? panic("Cannot borrow collection")
111            
112            for tokenId in self.NFTCollection! {
113                    let token <- _NFTCollection.withdraw(withdrawID: tokenId)
114                    adminTopShotReceiverRef.deposit(token: <-token)
115            }
116
117            assert(_NFTCollection.getIDs().length == 0, message: "can't destroy resources")
118            destroy _NFTCollection
119        }
120
121        pub fun isRedeemable(): Bool {
122            return getCurrentBlock().timestamp > self.startTime + self.duration
123        }
124
125        pub fun hasBeenRedeemed(): Bool {
126            return self.redeemed
127        }
128        
129
130        pub fun redeem(NFTIds: [UInt64]) {
131
132            pre {
133                !self.lock: "Reentrant call"
134                self.isRedeemable() : "Not redeemable yet"
135                !self.hasBeenRedeemed() : "Has already been redeemed"
136            }
137            post {
138                !self.lock: "Lock not released"
139            }
140            self.lock = true
141
142            let collectionRef = self.receiverCap.borrow() 
143                                ?? panic("Cannot borrow receiver")
144
145            let providerTopShotProviderRef: &TopShot.Collection? = TopShotEscrowV2.account.borrow<&TopShot.Collection>(from: /storage/MomentCollection) 
146                    ?? panic("Cannot borrow collection")
147
148            if (NFTIds.length == 0 || NFTIds.length == self.NFTCollection?.length){
149                // Iterate through the keys in the collection and deposit each one
150                for tokenId in self.NFTCollection! {
151                    let token <- providerTopShotProviderRef?.withdraw(withdrawID: tokenId)!
152                    collectionRef.deposit(token: <-token)
153                }
154
155                self.redeemed = true;
156
157                emit Redeemed(id: self.escrowID, owner: self.receiverCap.address, NFTIds: self.NFTCollection!, partial: false, time: getCurrentBlock().timestamp)
158            } else {
159                for NFTId in NFTIds {
160                    let token <- providerTopShotProviderRef?.withdraw(withdrawID: NFTId)!
161                    collectionRef.deposit(token: <-token)
162                    let index = self.NFTCollection?.firstIndex(of: NFTId) ?? panic("NFT ID not found")
163                    let removedId = self.NFTCollection?.remove(at: index !) ?? panic("NFT ID not found")
164                    assert(removedId == NFTId, message: "NFT ID mismatch")
165                }
166
167                if (self.NFTCollection?.length == 0){
168                    self.redeemed = true;
169                }
170
171                emit Redeemed(id: self.escrowID, owner: self.receiverCap.address, NFTIds: NFTIds, partial: !self.redeemed, time: getCurrentBlock().timestamp)
172            }
173
174            self.lock = false
175        }
176
177        pub fun getEscrowDetails(): EscrowDetails{
178            return EscrowDetails(
179                    _owner: self.receiverCap.address,
180                    _escrowID: self.escrowID,
181                    _NFTIds: self.NFTCollection,
182                    _startTime: self.startTime,
183                    _duration: self.duration,
184                    _isRedeemable: self.isRedeemable()
185                    )   
186        }
187
188        pub fun setEscrowDuration(_ newDuration: UFix64) {
189            post {
190                newDuration < self.duration : "Can only decrease duration"
191            }
192            self.duration = newDuration
193        }
194
195        pub fun cancelEscrow(NFTIds: [UInt64]) {
196            pre {
197                !self.hasBeenRedeemed() : "Has already been redeemed"
198            }
199            let collectionRef = self.receiverCap.borrow() 
200                                ?? panic("Cannot borrow receiver")
201
202            let providerTopShotProviderRef: &TopShot.Collection? = TopShotEscrowV2.account.borrow<&TopShot.Collection>(from: /storage/MomentCollection) 
203                    ?? panic("Cannot borrow collection")
204
205            if (NFTIds.length == 0 || NFTIds.length == self.NFTCollection?.length){
206                self.redeemed = true;
207
208                for tokenId in self.NFTCollection! {
209                    let token <- providerTopShotProviderRef?.withdraw(withdrawID: tokenId)!
210                    collectionRef.deposit(token: <-token)
211                }
212
213                emit EscrowCancelled(id: self.escrowID, owner: self.receiverCap.address, NFTIds: self.NFTCollection!, partial: false, time: getCurrentBlock().timestamp)
214            } else {
215                for NFTId in NFTIds {
216                    let token <- providerTopShotProviderRef?.withdraw(withdrawID: NFTId)!
217                    collectionRef.deposit(token: <-token)
218                    let index = self.NFTCollection?.firstIndex(of: NFTId) ?? panic("NFT ID not found")
219                    let removedId = self.NFTCollection?.remove(at: index !) ?? panic("NFT ID not found")
220                    assert(removedId == NFTId, message: "NFT ID mismatch")
221                }
222
223                if (self.NFTCollection?.length == 0){
224                    self.redeemed = true;
225                }
226
227                emit EscrowCancelled(id: self.escrowID, owner: self.receiverCap.address, NFTIds: NFTIds, partial: !self.redeemed, time: getCurrentBlock().timestamp)
228            }
229        }
230
231        pub fun addNFTs(NFTCollection: @TopShot.Collection) {
232            pre {
233                !self.hasBeenRedeemed() : "Has already been redeemed"
234            }
235            let NFTIds = NFTCollection.getIDs()
236            let adminTopShotReceiverRef = TopShotEscrowV2.account.getCapability(/public/MomentCollection).borrow<&{NonFungibleToken.Receiver, NonFungibleToken.CollectionPublic, TopShot.MomentCollectionPublic}>()
237            ?? panic("Cannot borrow collection")
238            
239            for NFTId in NFTIds {
240                    let token <- NFTCollection.withdraw(withdrawID: NFTId)
241                    adminTopShotReceiverRef.deposit(token: <-token)
242            }
243
244            assert(NFTCollection.getIDs().length == 0, message: "can't destroy resources")
245            destroy NFTCollection
246
247            self.NFTCollection!.appendAll(NFTIds)
248            emit EscrowUpdated(id: self.escrowID, owner: self.receiverCap.address, NFTIds: self.NFTCollection!)
249        }
250
251        destroy() { 
252            assert(self.redeemed, message: "Escrow not redeemed")
253        }
254    }
255
256    // An interface to interact publicly with the Escrow Collection
257    pub resource interface EscrowCollectionPublic {
258        pub fun createEscrow(
259                NFTCollection: @TopShot.Collection,
260                duration: UFix64,
261                receiverCap : Capability<&{NonFungibleToken.Receiver}>)
262
263        pub fun borrowEscrow(escrowID: UInt64): &EscrowItem{EscrowItemPublic}?
264        pub fun getEscrowIDs() : [UInt64]
265    }
266
267    // EscrowCollection contains a dictionary of EscrowItems 
268    // and provides methods for manipulating the EscrowItems
269    pub resource EscrowCollection: EscrowCollectionPublic {
270
271        // Escrow Items
272        access(self) var escrowItems: @{UInt64: EscrowItem}
273
274        // withdraw
275        // Removes an escrow from the collection and moves it to the caller
276        pub fun withdraw(escrowID: UInt64): @TopShotEscrowV2.EscrowItem {
277            let escrow <- self.escrowItems.remove(key: escrowID) ?? panic("missing NFT")
278
279            emit EscrowWithdraw(id: escrow.escrowID, from: self.owner?.address)
280
281            return <-escrow
282        }
283
284
285        init() {
286            self.escrowItems <- {}
287        }
288
289        pub fun getEscrowIDs() : [UInt64] {
290            return self.escrowItems.keys
291        }
292
293        pub fun createEscrow(
294                NFTCollection: @TopShot.Collection,
295                duration: UFix64,
296                receiverCap : Capability<&{NonFungibleToken.Receiver}>) {
297
298            let TopShotIds = NFTCollection.getIDs()
299
300            assert(receiverCap.check(), message : "Non Valid Receiver Capability")
301
302            // create a new escrow item resource container
303            let item <- create EscrowItem(
304                _NFTCollection: <- NFTCollection,
305                _duration: duration,
306                _receiverCap: receiverCap)
307
308            let escrowID = item.escrowID
309            let startTime = item.startTime
310            // update the escrow items dictionary with the new resources
311            let oldItem <- self.escrowItems[escrowID] <- item
312            destroy oldItem
313
314            let owner = receiverCap.address
315
316            emit Escrowed(id: escrowID, owner: owner, NFTIds: TopShotIds, duration: duration, startTime: startTime)
317        }
318
319        pub fun borrowEscrow(escrowID: UInt64): &EscrowItem{EscrowItemPublic}? {
320            // Get the escrow item resources
321            if let escrowRef = (&self.escrowItems[escrowID] as &EscrowItem{EscrowItemPublic}?) {
322                return escrowRef
323            }
324            return nil
325        }
326
327        pub fun createEscrowRef(escrowID: UInt64): &EscrowItem {
328            // Get the escrow item resources
329            let escrowRef = (&self.escrowItems[escrowID] as &EscrowItem?)!
330            return escrowRef
331        }
332
333        destroy() {
334            assert(self.escrowItems.length == 0, message: "Escrow items still exist")
335            destroy self.escrowItems
336        }
337    }
338
339    // -----------------------------------------------------------------------
340    // TopShotEscrowV2 contract-level function definitions
341    // -----------------------------------------------------------------------
342
343    // createEscrowCollection returns a new EscrowCollection resource to the caller
344    pub fun createEscrowCollection(): @EscrowCollection {
345        let escrowCollection <- create EscrowCollection()
346
347        return <- escrowCollection
348    }
349
350    // createEscrow
351    pub fun createEscrow(
352                _ NFTCollection: @TopShot.Collection,
353                _ duration: UFix64,
354                _ receiverCap : Capability<&{NonFungibleToken.Receiver}>) {
355        let escrowCollectionRef = self.account.borrow<&TopShotEscrowV2.EscrowCollection>(from: self.escrowStoragePath) ??
356                                                                    panic("Couldn't borrow escrow collection")
357        escrowCollectionRef.createEscrow(NFTCollection: <- NFTCollection, duration: duration, receiverCap: receiverCap)
358    }
359
360    // redeem tokens
361    pub fun redeem(_ escrowID: UInt64, _ NFTIds: [UInt64]) {
362        let escrowCollectionRef = self.account.borrow<&TopShotEscrowV2.EscrowCollection>(from: self.escrowStoragePath) ??
363                                                                    panic("Couldn't borrow escrow collection")
364        let escrowRef = escrowCollectionRef.borrowEscrow(escrowID: escrowID)!
365        escrowRef.redeem(NFTIds: NFTIds)
366        if (escrowRef.redeemed){
367            destroy <- escrowCollectionRef.withdraw(escrowID: escrowID)
368        }
369    }
370
371    // batch redeem tokens
372    pub fun batchRedeem(_ escrowIDs: [UInt64]) {
373
374        for escrowID in escrowIDs {
375            let escrowCollectionRef = self.account.borrow<&TopShotEscrowV2.EscrowCollection>(from: self.escrowStoragePath) ??
376                                                                        panic("Couldn't borrow escrow collection")
377            let escrowRef = escrowCollectionRef.borrowEscrow(escrowID: escrowID)!
378            escrowRef.redeem(NFTIds: [])
379            if (escrowRef.redeemed){
380            destroy <- escrowCollectionRef.withdraw(escrowID: escrowID)
381            }
382        }
383    }
384
385    // -----------------------------------------------------------------------
386    // TopShotEscrowV2 initialization function
387    // -----------------------------------------------------------------------
388    //
389
390    init() {
391        self.totalEscrows = 0
392        self.escrowStoragePath= /storage/TopShotEscrowV2
393        self.escrowPublicPath= /public/TopShotEscrowV2
394
395        // Setup collection onto Deployer's account
396        let escrowCollection <- self.createEscrowCollection()
397        self.account.save(<- escrowCollection, to: self.escrowStoragePath)
398        self.account.link<&TopShotEscrowV2.EscrowCollection{TopShotEscrowV2.EscrowCollectionPublic}>(self.escrowPublicPath, target: self.escrowStoragePath)
399    }   
400}
401